summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/home_controller.rb18
-rw-r--r--app/javascript/mastodon/actions/accounts.js6
-rw-r--r--app/javascript/mastodon/actions/markers.js10
-rw-r--r--app/javascript/mastodon/components/logo.js4
-rw-r--r--app/javascript/mastodon/containers/mastodon.js2
-rw-r--r--app/javascript/mastodon/features/account/components/header.js30
-rw-r--r--app/javascript/mastodon/features/community_timeline/index.js6
-rw-r--r--app/javascript/mastodon/features/directory/index.js6
-rw-r--r--app/javascript/mastodon/features/explore/index.js6
-rw-r--r--app/javascript/mastodon/features/explore/links.js11
-rw-r--r--app/javascript/mastodon/features/explore/results.js18
-rw-r--r--app/javascript/mastodon/features/explore/suggestions.js11
-rw-r--r--app/javascript/mastodon/features/explore/tags.js11
-rw-r--r--app/javascript/mastodon/features/hashtag_timeline/index.js18
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js6
-rw-r--r--app/javascript/mastodon/features/status/index.js24
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/compose_panel.js22
-rw-r--r--app/javascript/mastodon/features/ui/components/document_title.js41
-rw-r--r--app/javascript/mastodon/features/ui/components/link_footer.js46
-rw-r--r--app/javascript/mastodon/features/ui/components/navigation_panel.js83
-rw-r--r--app/javascript/mastodon/features/ui/components/sign_in_banner.js11
-rw-r--r--app/javascript/mastodon/features/ui/index.js43
-rw-r--r--app/javascript/mastodon/initial_state.js2
-rw-r--r--app/javascript/styles/mastodon/_mixins.scss1
-rw-r--r--app/javascript/styles/mastodon/components.scss56
-rw-r--r--app/lib/permalink_redirector.rb4
-rw-r--r--app/serializers/initial_state_serializer.rb11
-rw-r--r--app/views/home/index.html.haml12
-rw-r--r--package.json1
-rw-r--r--spec/controllers/home_controller_spec.rb20
-rw-r--r--spec/lib/permalink_redirector_spec.rb4
-rw-r--r--yarn.lock20
33 files changed, 423 insertions, 145 deletions
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 7e443eb9e74..29478209dbf 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,8 +2,8 @@
class HomeController < ApplicationController
before_action :redirect_unauthenticated_to_permalinks!
- before_action :authenticate_user!
before_action :set_referrer_policy_header
+ before_action :set_instance_presenter
def index
@body_classes = 'app-body'
@@ -14,20 +14,16 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks!
return if user_signed_in?
- redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
- end
+ redirect_path = PermalinkRedirector.new(request.path).redirect_path
- def default_redirect_path
- if request.path.start_with?('/web') || whitelist_mode?
- new_user_session_path
- elsif single_user_mode?
- short_account_path(Account.local.without_suspended.where('id > 0').first)
- else
- about_path
- end
+ redirect_to(redirect_path) if redirect_path.present?
end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
+
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index eedf61dc99e..f61f06e408f 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -536,10 +536,12 @@ export function expandFollowingFail(id, error) {
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
- const loadedRelationships = getState().get('relationships');
+ const state = getState();
+ const loadedRelationships = state.get('relationships');
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+ const signedIn = !!state.getIn(['meta', 'me']);
- if (newAccountIds.length === 0) {
+ if (!signedIn || newAccountIds.length === 0) {
return;
}
diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js
index 16a3df8f638..b7f406cb86b 100644
--- a/app/javascript/mastodon/actions/markers.js
+++ b/app/javascript/mastodon/actions/markers.js
@@ -1,6 +1,7 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
+import { List as ImmutableList } from 'immutable';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
@@ -11,7 +12,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
@@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => {
const params = {};
- const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
+ const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
@@ -82,9 +83,10 @@ const _buildParams = (state) => {
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
- const params = _buildParams(getState());
+ const accessToken = getState().getIn(['meta', 'access_token'], '');
+ const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js
index d1c7f08a91d..3570b3644b7 100644
--- a/app/javascript/mastodon/components/logo.js
+++ b/app/javascript/mastodon/components/logo.js
@@ -1,8 +1,8 @@
import React from 'react';
const Logo = () => (
- <svg viewBox='0 0 216.4144 232.00976' className='logo'>
- <use xlinkHref='#mastodon-svg-logo' />
+ <svg viewBox='0 0 261 66' className='logo'>
+ <use xlinkHref='#logo-symbol-wordmark' />
</svg>
);
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index f4bef46863a..08241522cf6 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -26,7 +26,7 @@ const createIdentityContext = state => ({
signedIn: !!state.meta.me,
accountId: state.meta.me,
accessToken: state.meta.access_token,
- permissions: state.role.permissions,
+ permissions: state.role ? state.role.permissions : 0,
});
export default class Mastodon extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 8f2753c35ef..e407a0d55c6 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { autoPlayGif, me } from 'mastodon/initial_state';
+import { autoPlayGif, me, title, domain } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
@@ -15,6 +15,7 @@ import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -54,6 +55,14 @@ const messages = defineMessages({
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
});
+const titleFromAccount = account => {
+ const displayName = account.get('display_name');
+ const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
+ const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
+
+ return `${prefix} (@${acct})`;
+};
+
const dateFormatOptions = {
month: 'short',
day: 'numeric',
@@ -132,6 +141,7 @@ class Header extends ImmutablePureComponent {
render () {
const { account, hidden, intl, domain } = this.props;
+ const { signedIn } = this.context.identity;
if (!account) {
return null;
@@ -162,12 +172,12 @@ class Header extends ImmutablePureComponent {
}
if (me !== account.get('id')) {
- if (!account.get('relationship')) { // Wait until the relationship is loaded
+ if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
- actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} 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 disabled={account.getIn(['relationship', 'blocked_by'])} 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 : undefined} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
@@ -183,7 +193,7 @@ class Header extends ImmutablePureComponent {
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
}
- if (account.get('id') !== me) {
+ if (signedIn && account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
@@ -206,7 +216,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
- } else {
+ } else if (signedIn) {
if (account.getIn(['relationship', 'following'])) {
if (!account.getIn(['relationship', 'muting'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
@@ -239,7 +249,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
}
- if (account.get('acct') !== account.get('username')) {
+ if (signedIn && account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
@@ -298,7 +308,7 @@ class Header extends ImmutablePureComponent {
</React.Fragment>
)}
- <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
+ <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
)}
</div>
@@ -327,7 +337,7 @@ class Header extends ImmutablePureComponent {
</div>
)}
- {account.get('id') !== me && <AccountNoteContainer account={account} />}
+ {(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
@@ -359,6 +369,10 @@ class Header extends ImmutablePureComponent {
</div>
)}
</div>
+
+ <Helmet>
+ <title>{titleFromAccount(account)} - {title}</title>
+ </Helmet>
</div>
);
}
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index 30f77604867..f9d50e64cec 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -9,6 +9,8 @@ import { expandCommunityTimeline } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectCommunityStream } from '../../actions/streaming';
+import { Helmet } from 'react-helmet';
+import { title } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
@@ -128,6 +130,10 @@ class CommunityTimeline extends React.PureComponent {
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
bindToDocument={!multiColumn}
/>
+
+ <Helmet>
+ <title>{intl.formatMessage(messages.title)} - {title}</title>
+ </Helmet>
</Column>
);
}
diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js
index 94d7d1a9c54..36f46c510e0 100644
--- a/app/javascript/mastodon/features/directory/index.js
+++ b/app/javascript/mastodon/features/directory/index.js
@@ -13,6 +13,8 @@ import RadioButton from 'mastodon/components/radio_button';
import LoadMore from 'mastodon/components/load_more';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadingIndicator from 'mastodon/components/loading_indicator';
+import { title } from 'mastodon/initial_state';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@@ -165,6 +167,10 @@ class Directory extends React.PureComponent {
/>
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
+
+ <Helmet>
+ <title>{intl.formatMessage(messages.title)} - {title}</title>
+ </Helmet>
</Column>
);
}
diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js
index 8082f2d99cb..e1d1eb563f2 100644
--- a/app/javascript/mastodon/features/explore/index.js
+++ b/app/javascript/mastodon/features/explore/index.js
@@ -11,6 +11,8 @@ import Statuses from './statuses';
import Suggestions from './suggestions';
import Search from 'mastodon/features/compose/containers/search_container';
import SearchResults from './results';
+import { Helmet } from 'react-helmet';
+import { title } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'explore.title', defaultMessage: 'Explore' },
@@ -81,6 +83,10 @@ class Explore extends React.PureComponent {
<Route path='/explore/suggestions' component={Suggestions} />
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
</Switch>
+
+ <Helmet>
+ <title>{intl.formatMessage(messages.title)} - {title}</title>
+ </Helmet>
</React.Fragment>
)}
</div>
diff --git a/app/javascript/mastodon/features/explore/links.js b/app/javascript/mastodon/features/explore/links.js
index 6649fb6e471..d3aaa9cddf7 100644
--- a/app/javascript/mastodon/features/explore/links.js
+++ b/app/javascript/mastodon/features/explore/links.js
@@ -5,6 +5,7 @@ import Story from './components/story';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import { connect } from 'react-redux';
import { fetchTrendingLinks } from 'mastodon/actions/trends';
+import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({
links: state.getIn(['trends', 'links', 'items']),
@@ -28,6 +29,16 @@ class Links extends React.PureComponent {
render () {
const { isLoading, links } = this.props;
+ if (!isLoading && links.isEmpty()) {
+ return (
+ <div className='explore__links scrollable scrollable--flex'>
+ <div className='empty-column-indicator'>
+ <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
+ </div>
+ </div>
+ );
+ }
+
return (
<div className='explore__links'>
{isLoading ? (<LoadingIndicator />) : links.map(link => (
diff --git a/app/javascript/mastodon/features/explore/results.js b/app/javascript/mastodon/features/explore/results.js
index 1286020f54c..0dc108918d2 100644
--- a/app/javascript/mastodon/features/explore/results.js
+++ b/app/javascript/mastodon/features/explore/results.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { expandSearch } from 'mastodon/actions/search';
import Account from 'mastodon/containers/account_container';
@@ -10,10 +10,17 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { List as ImmutableList } from 'immutable';
import LoadMore from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator';
+import { title } from 'mastodon/initial_state';
+import { Helmet } from 'react-helmet';
+
+const messages = defineMessages({
+ title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
+});
const mapStateToProps = state => ({
isLoading: state.getIn(['search', 'isLoading']),
results: state.getIn(['search', 'results']),
+ q: state.getIn(['search', 'searchTerm']),
});
const appendLoadMore = (id, list, onLoadMore) => {
@@ -37,6 +44,7 @@ const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', resul
)), onLoadMore);
export default @connect(mapStateToProps)
+@injectIntl
class Results extends React.PureComponent {
static propTypes = {
@@ -44,6 +52,8 @@ class Results extends React.PureComponent {
isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
+ q: PropTypes.string,
+ intl: PropTypes.object,
};
state = {
@@ -64,7 +74,7 @@ class Results extends React.PureComponent {
}
render () {
- const { isLoading, results } = this.props;
+ const { intl, isLoading, q, results } = this.props;
const { type } = this.state;
let filteredResults = ImmutableList();
@@ -106,6 +116,10 @@ class Results extends React.PureComponent {
<div className='explore__search-results'>
{isLoading ? <LoadingIndicator /> : filteredResults}
</div>
+
+ <Helmet>
+ <title>{intl.formatMessage(messages.title, { q })} - {title}</title>
+ </Helmet>
</React.Fragment>
);
}
diff --git a/app/javascript/mastodon/features/explore/suggestions.js b/app/javascript/mastodon/features/explore/suggestions.js
index 0c6a7ef8a3b..e6ad09974c8 100644
--- a/app/javascript/mastodon/features/explore/suggestions.js
+++ b/app/javascript/