Add logged-out access to the web UI (#18961)
This commit is contained in:
		
							parent
							
								
									1a5150e9c3
								
							
						
					
					
						commit
						43b5d5e38d
					
				| @ -2,8 +2,8 @@ | |||||||
| 
 | 
 | ||||||
| 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_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' | ||||||
| @ -14,20 +14,16 @@ 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 | ||||||
|   end |  | ||||||
| 
 | 
 | ||||||
|   def default_redirect_path |     redirect_to(redirect_path) if redirect_path.present? | ||||||
|     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 |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   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 | ||||||
|  | |||||||
| @ -536,10 +536,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 '../api'; | import api from '../api'; | ||||||
| import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||||
| import compareId from '../compare_id'; | import compareId from '../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,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> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -26,7 +26,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 { | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import PropTypes from 'prop-types'; | |||||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||||
| import Button from 'mastodon/components/button'; | import Button from 'mastodon/components/button'; | ||||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | 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 classNames from 'classnames'; | ||||||
| import Icon from 'mastodon/components/icon'; | import Icon from 'mastodon/components/icon'; | ||||||
| import IconButton from 'mastodon/components/icon_button'; | 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 DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; | ||||||
| import AccountNoteContainer from '../containers/account_note_container'; | import AccountNoteContainer from '../containers/account_note_container'; | ||||||
| import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; | import { PERMISSION_MANAGE_USERS } from 'mastodon/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', | ||||||
| @ -132,6 +141,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; | ||||||
| @ -162,12 +172,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 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'])) { |       } 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} />; | ||||||
|       } |       } | ||||||
| @ -183,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) { |     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.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); | ||||||
| @ -206,7 +216,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'])) { | ||||||
| @ -239,7 +249,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); | ||||||
| @ -298,7 +308,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> | ||||||
| @ -327,7 +337,7 @@ class Header extends ImmutablePureComponent { | |||||||
|                   </div> |                   </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} />} |                 {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> | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|  | 
 | ||||||
|  |         <Helmet> | ||||||
|  |           <title>{titleFromAccount(account)} - {title}</title> | ||||||
|  |         </Helmet> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -9,6 +9,8 @@ import { expandCommunityTimeline } from '../../actions/timelines'; | |||||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | import ColumnSettingsContainer from './containers/column_settings_container'; | ||||||
| import { connectCommunityStream } from '../../actions/streaming'; | import { connectCommunityStream } from '../../actions/streaming'; | ||||||
|  | import { Helmet } from 'react-helmet'; | ||||||
|  | import { title } from 'mastodon/initial_state'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   title: { id: 'column.community', defaultMessage: 'Local timeline' }, |   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!' />} |           emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} | ||||||
|           bindToDocument={!multiColumn} |           bindToDocument={!multiColumn} | ||||||
|         /> |         /> | ||||||
|  | 
 | ||||||
|  |         <Helmet> | ||||||
|  |           <title>{intl.formatMessage(messages.title)} - {title}</title> | ||||||
|  |         </Helmet> | ||||||
|       </Column> |       </Column> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -13,6 +13,8 @@ import RadioButton from 'mastodon/components/radio_button'; | |||||||
| import LoadMore from 'mastodon/components/load_more'; | import LoadMore from 'mastodon/components/load_more'; | ||||||
| import ScrollContainer from 'mastodon/containers/scroll_container'; | import ScrollContainer from 'mastodon/containers/scroll_container'; | ||||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||||
|  | import { title } from 'mastodon/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> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -11,6 +11,8 @@ import Statuses from './statuses'; | |||||||
| import Suggestions from './suggestions'; | import Suggestions from './suggestions'; | ||||||
| import Search from 'mastodon/features/compose/containers/search_container'; | import Search from 'mastodon/features/compose/containers/search_container'; | ||||||
| import SearchResults from './results'; | import SearchResults from './results'; | ||||||
|  | import { Helmet } from 'react-helmet'; | ||||||
|  | import { title } from 'mastodon/initial_state'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   title: { id: 'explore.title', defaultMessage: 'Explore' }, |   title: { id: 'explore.title', defaultMessage: 'Explore' }, | ||||||
| @ -81,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 'mastodon/components/loading_indicator'; | import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import { fetchTrendingLinks } from 'mastodon/actions/trends'; | import { fetchTrendingLinks } from 'mastodon/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 'mastodon/actions/search'; | import { expandSearch } from 'mastodon/actions/search'; | ||||||
| import Account from 'mastodon/containers/account_container'; | 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 { List as ImmutableList } from 'immutable'; | ||||||
| import LoadMore from 'mastodon/components/load_more'; | import LoadMore from 'mastodon/components/load_more'; | ||||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | 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 => ({ | 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 'mastodon/features/directory/components/account_card'; | |||||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import { fetchSuggestions } from 'mastodon/actions/suggestions'; | import { fetchSuggestions } from 'mastodon/actions/suggestions'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
| const mapStateToProps = state => ({ | const mapStateToProps = state => ({ | ||||||
|   suggestions: state.getIn(['suggestions', 'items']), |   suggestions: state.getIn(['suggestions', 'items']), | ||||||
| @ -28,6 +29,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 'mastodon/components/hashtag'; | |||||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import { fetchTrendingHashtags } from 'mastodon/actions/trends'; | import { fetchTrendingHashtags } from 'mastodon/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 => ( | ||||||
|  | |||||||
| @ -14,6 +14,8 @@ import { isEqual } from 'lodash'; | |||||||
| import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags'; | import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags'; | ||||||
| import Icon from 'mastodon/components/icon'; | import Icon from 'mastodon/components/icon'; | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
|  | import { title } from 'mastodon/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, | ||||||
| @ -158,6 +164,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 +181,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 +189,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 +220,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> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -9,6 +9,8 @@ import { expandPublicTimeline } from '../../actions/timelines'; | |||||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | import ColumnSettingsContainer from './containers/column_settings_container'; | ||||||
| import { connectPublicStream } from '../../actions/streaming'; | import { connectPublicStream } from '../../actions/streaming'; | ||||||
|  | import { Helmet } from 'react-helmet'; | ||||||
|  | import { title } from 'mastodon/initial_state'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   title: { id: 'column.public', defaultMessage: 'Federated timeline' }, |   title: { id: 'column.public', defaultMessage: 'Federated timeline' }, | ||||||
| @ -131,6 +133,10 @@ class PublicTimeline extends React.PureComponent { | |||||||
|           emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} |           emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} | ||||||
|           bindToDocument={!multiColumn} |           bindToDocument={!multiColumn} | ||||||
|         /> |         /> | ||||||
|  | 
 | ||||||
|  |         <Helmet> | ||||||
|  |           <title>{intl.formatMessage(messages.title)} - {title}</title> | ||||||
|  |         </Helmet> | ||||||
|       </Column> |       </Column> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -56,10 +56,11 @@ import { openModal } from '../../actions/modal'; | |||||||
| 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 { HotKeys } from 'react-hotkeys'; | import { HotKeys } from 'react-hotkeys'; | ||||||
| import { boostModal, deleteModal } from '../../initial_state'; | import { boostModal, deleteModal, title } from '../../initial_state'; | ||||||
| import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; | import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; | ||||||
| import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; | import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; | ||||||
| import Icon from 'mastodon/components/icon'; | import Icon from 'mastodon/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' }, | ||||||
| @ -156,6 +157,23 @@ 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 { | ||||||
| @ -605,6 +623,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 = { | ||||||
| @ -212,11 +213,12 @@ class ColumnsArea extends ImmutablePureComponent { | |||||||
|   render () { |   render () { | ||||||
|     const { columns, children, singleColumn, isModalOpen, intl } = this.props; |     const { columns, children, singleColumn, isModalOpen, intl } = 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}> | ||||||
|  | |||||||
| @ -10,6 +10,10 @@ import { changeComposing } from 'mastodon/actions/compose'; | |||||||
| export default @connect() | export default @connect() | ||||||
| class ComposePanel extends React.PureComponent { | class ComposePanel extends React.PureComponent { | ||||||
| 
 | 
 | ||||||
|  |   static contextTypes = { | ||||||
|  |     identity: PropTypes.object.isRequired, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     dispatch: PropTypes.func.isRequired, |     dispatch: PropTypes.func.isRequired, | ||||||
|   }; |   }; | ||||||
| @ -23,11 +27,25 @@ class ComposePanel extends React.PureComponent { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render() { |   render() { | ||||||
|  |     const { signedIn } = this.context.identity; | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className='compose-panel' onFocus={this.onFocus}> |       <div className='compose-panel' onFocus={this.onFocus}> | ||||||
|         <SearchContainer openInRoute /> |         <SearchContainer openInRoute /> | ||||||
|  | 
 | ||||||
|  |         {!signedIn && ( | ||||||
|  |           <React.Fragment> | ||||||
|  |             <div className='flex-spacer' /> | ||||||
|  |           </React.Fragment> | ||||||
|  |         )} | ||||||
|  | 
 | ||||||
|  |         {signedIn && ( | ||||||
|  |           <React.Fragment> | ||||||
|             <NavigationContainer onClose={this.onBlur} /> |             <NavigationContainer onClose={this.onBlur} /> | ||||||
|             <ComposeFormContainer singleColumn /> |             <ComposeFormContainer singleColumn /> | ||||||
|  |           </React.Fragment> | ||||||
|  |         )} | ||||||
|  | 
 | ||||||
|         <LinkFooter withHotkeys /> |         <LinkFooter withHotkeys /> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -1,41 +0,0 @@ | |||||||
| import { PureComponent } from 'react'; |  | ||||||
| import { connect } from 'react-redux'; |  | ||||||
| import PropTypes from 'prop-types'; |  | ||||||
| import { title } from 'mastodon/initial_state'; |  | ||||||
| 
 |  | ||||||
| const mapStateToProps = state => ({ |  | ||||||
|   unread: state.getIn(['missed_updates', 'unread']), |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export default @connect(mapStateToProps) |  | ||||||
| class DocumentTitle extends PureComponent { |  | ||||||
| 
 |  | ||||||
|   static propTypes = { |  | ||||||
|     unread: PropTypes.number.isRequired, |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   componentDidMount () { |  | ||||||
|     this._sideEffects(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   componentDidUpdate() { |  | ||||||
|     this._sideEffects(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   _sideEffects () { |  | ||||||
|     const { unread } = this.props; |  | ||||||
| 
 |  | ||||||
|     if (unread > 99) { |  | ||||||
|       document.title = `(*) ${title}`; |  | ||||||
|     } else if (unread > 0) { |  | ||||||
|       document.title = `(${unread}) ${title}`; |  | ||||||
|     } else { |  | ||||||
|       document.title = title; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   render () { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -49,20 +49,46 @@ class LinkFooter extends React.PureComponent { | |||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { withHotkeys } = this.props; |     const { withHotkeys } = this.props; | ||||||
|  |     const { signedIn, permissions } = this.context.identity; | ||||||
|  |     const items = []; | ||||||
|  | 
 | ||||||
|  |     if ((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 (withHotkeys) { | ||||||
|  |       items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (signedIn) { | ||||||
|  |       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>); | ||||||
|  |     items.push(<a key='terms' href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></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>} |           <li>{items.reduce((prev, curr) => [prev, ' · ', curr])}</li> | ||||||
|           {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} |  | ||||||
|           <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> |  | ||||||
|           {!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>} |  | ||||||
|           {profileDirectory && <li><Link to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link> · </li>} |  | ||||||
|           <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> |  | ||||||
|           <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></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='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> |  | ||||||
|         </ul> |         </ul> | ||||||
| 
 | 
 | ||||||
|         <p> |         <p> | ||||||
|  | |||||||
| @ -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 'mastodon/components/icon'; | import Icon from 'mastodon/components/icon'; | ||||||
| import { showTrends } from 'mastodon/initial_state'; | import { showTrends } from 'mastodon/initial_state'; | ||||||
| @ -7,15 +8,46 @@ 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 'mastodon/features/getting_started/containers/trends_container'; | import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container'; | ||||||
|  | import Logo from 'mastodon/components/logo'; | ||||||
|  | import SignInBanner from './sign_in_banner'; | ||||||
| 
 | 
 | ||||||
| const NavigationPanel = () => ( | export default class NavigationPanel extends React.Component { | ||||||
|  | 
 | ||||||
|  |   static contextTypes = { | ||||||
|  |     router: PropTypes.object.isRequired, | ||||||
|  |     identity: PropTypes.object.isRequired, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   render () { | ||||||
|  |     const { signedIn } = this.context.identity; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|       <div className='navigation-panel'> |       <div className='navigation-panel'> | ||||||
|  |         <Link to='/' className='column-link column-link--logo'><Logo /></Link> | ||||||
|  | 
 | ||||||
|  |         <hr /> | ||||||
|  | 
 | ||||||
|  |         {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> | ||||||
|  |         )} | ||||||
|  | 
 | ||||||
|         <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='/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='at' 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='at' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> | ||||||
|             <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> |             <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></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> | ||||||
| @ -27,10 +59,17 @@ const NavigationPanel = () => ( | |||||||
| 
 | 
 | ||||||
|             <a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> |             <a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> | ||||||
|             <a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> |             <a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> | ||||||
|  |           </React.Fragment> | ||||||
|  |         )} | ||||||
| 
 | 
 | ||||||
|     {showTrends && <div className='flex-spacer' />} |         {showTrends && ( | ||||||
|     {showTrends && <TrendsContainer />} |           <React.Fragment> | ||||||
|  |             <div className='flex-spacer' /> | ||||||
|  |             <TrendsContainer /> | ||||||
|  |           </React.Fragment> | ||||||
|  |         )} | ||||||
|       </div> |       </div> | ||||||
| ); |     ); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
| export default withRouter(NavigationPanel); | } | ||||||
|  | |||||||
| @ -0,0 +1,11 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
|  | 
 | ||||||
|  | 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> | ||||||
|  |   </div> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default SignInBanner; | ||||||
| @ -20,7 +20,6 @@ import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodo | |||||||
| import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; | import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; | ||||||
| import UploadArea from './components/upload_area'; | import UploadArea from './components/upload_area'; | ||||||
| import ColumnsAreaContainer from './containers/columns_area_container'; | import ColumnsAreaContainer from './containers/columns_area_container'; | ||||||
| import DocumentTitle from './components/document_title'; |  | ||||||
| import PictureInPicture from 'mastodon/features/picture_in_picture'; | import PictureInPicture from 'mastodon/features/picture_in_picture'; | ||||||
| import { | import { | ||||||
|   Compose, |   Compose, | ||||||
| @ -53,8 +52,9 @@ import { | |||||||
|   Explore, |   Explore, | ||||||
|   FollowRecommendations, |   FollowRecommendations, | ||||||
| } from './util/async-components'; | } from './util/async-components'; | ||||||
| import { me } from '../../initial_state'; | import { me, title } from '../../initial_state'; | ||||||
| import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; | import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; | ||||||
|  | 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.
 | ||||||
| @ -110,6 +110,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, | ||||||
| @ -145,12 +149,25 @@ class SwitchingColumnsArea extends React.PureComponent { | |||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { children, mobile } = this.props; |     const { children, mobile } = 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}> |       <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> | ||||||
|         <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} /> | ||||||
| 
 | 
 | ||||||
| @ -208,6 +225,7 @@ class UI extends React.PureComponent { | |||||||
| 
 | 
 | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|     router: PropTypes.object.isRequired, |     router: PropTypes.object.isRequired, | ||||||
|  |     identity: PropTypes.object.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
| @ -343,6 +361,8 @@ class UI extends React.PureComponent { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   componentDidMount () { | ||||||
|  |     const { signedIn } = this.context.identity; | ||||||
|  | 
 | ||||||
|     window.addEventListener('focus', this.handleWindowFocus, false); |     window.addEventListener('focus', this.handleWindowFocus, false); | ||||||
|     window.addEventListener('blur', this.handleWindowBlur, false); |     window.addEventListener('blur', this.handleWindowBlur, false); | ||||||
|     window.addEventListener('beforeunload', this.handleBeforeUnload, false); |     window.addEventListener('beforeunload', this.handleBeforeUnload, false); | ||||||
| @ -359,16 +379,18 @@ class UI extends React.PureComponent { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 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(fetchRules()), 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); | ||||||
| @ -546,7 +568,10 @@ class UI extends React.PureComponent { | |||||||
|           <LoadingBarContainer className='loading-bar' /> |           <LoadingBarContainer className='loading-bar' /> | ||||||
|           <ModalContainer /> |           <ModalContainer /> | ||||||
|           <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> |           <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> | ||||||
|           <DocumentTitle /> | 
 | ||||||
|  |           <Helmet> | ||||||
|  |             <title>{title}</title> | ||||||
|  |           </Helmet> | ||||||
|         </div> |         </div> | ||||||
|       </HotKeys> |       </HotKeys> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ const initialState = element && JSON.parse(element.textContent); | |||||||
| 
 | 
 | ||||||
| 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'); | export const displayMedia = getMeta('display_media'); | ||||||
| @ -26,5 +27,6 @@ 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; | ||||||
|  | |||||||
| @ -20,6 +20,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; | ||||||
| } | } | ||||||
|  | |||||||
| @ -126,6 +126,7 @@ | |||||||
|     &: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 { |     &:disabled { | ||||||
| @ -700,6 +701,15 @@ | |||||||
|   transition: height 0.4s ease, opacity 0.4s ease; |   transition: height 0.4s ease, opacity 0.4s ease; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .sign-in-banner { | ||||||
|  |   padding: 10px; | ||||||
|  | 
 | ||||||
|  |   p { | ||||||
|  |     color: $darker-text-color; | ||||||
|  |     margin-bottom: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .emojione { | .emojione { | ||||||
|   font-size: inherit; |   font-size: inherit; | ||||||
|   vertical-align: middle; |   vertical-align: middle; | ||||||
| @ -2214,6 +2224,7 @@ a.account__display-name { | |||||||
| 
 | 
 | ||||||
|   > .scrollable { |   > .scrollable { | ||||||
|     background: $ui-base-color; |     background: $ui-base-color; | ||||||
|  |     border-radius: 0 0 4px 4px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -2660,6 +2671,26 @@ a.account__display-name { | |||||||
|   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; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .navigation-bar { |   .navigation-bar { | ||||||
|     padding-top: 20px; |     padding-top: 20px; | ||||||
|     padding-bottom: 20px; |     padding-bottom: 20px; | ||||||
| @ -2667,10 +2698,6 @@ a.account__display-name { | |||||||
|     min-height: 20px; |     min-height: 20px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .flex-spacer { |  | ||||||
|     background: transparent; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .compose-form { |   .compose-form { | ||||||
|     flex: 1; |     flex: 1; | ||||||
|     overflow-y: hidden; |     overflow-y: hidden; | ||||||
| @ -2709,6 +2736,14 @@ a.account__display-name { | |||||||
|     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; | ||||||
| @ -2836,6 +2871,7 @@ a.account__display-name { | |||||||
|   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; | ||||||
| @ -3031,6 +3067,17 @@ a.account__display-name { | |||||||
|       color: $highlight-text-color; |       color: $highlight-text-color; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   &--logo { | ||||||
|  |     background: transparent; | ||||||
|  |     padding: 10px; | ||||||
|  | 
 | ||||||
|  |     &:hover, | ||||||
|  |     &:focus, | ||||||
|  |     &:active { | ||||||
|  |       background: transparent; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .column-link__icon { | .column-link__icon { | ||||||
| @ -3551,6 +3598,7 @@ a.status-card.compact:hover { | |||||||
|   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; | ||||||
|  | |||||||
| @ -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,9 +1,11 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class InitialStateSerializer < ActiveModel::Serializer | class InitialStateSerializer < ActiveModel::Serializer | ||||||
|  |   include RoutingHelper | ||||||
|  | 
 | ||||||
|   attributes :meta, :compose, :accounts, |   attributes :meta, :compose, :accounts, | ||||||
|              :media_attachments, :settings, |              :media_attachments, :settings, | ||||||
|              :languages |              :languages, :server | ||||||
| 
 | 
 | ||||||
|   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 | ||||||
| @ -82,6 +84,13 @@ 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 | ||||||
|  | |||||||
| @ -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 | ||||||
|   = javascript_pack_tag 'application', crossorigin: 'anonymous' |   = javascript_pack_tag 'application', crossorigin: 'anonymous' | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -92,6 +92,7 @@ | |||||||
|     "punycode": "^2.1.0", |     "punycode": "^2.1.0", | ||||||
|     "react": "^16.14.0", |     "react": "^16.14.0", | ||||||
|     "react-dom": "^16.14.0", |     "react-dom": "^16.14.0", | ||||||
|  |     "react-helmet": "^6.1.0", | ||||||
|     "react-hotkeys": "^1.1.4", |     "react-hotkeys": "^1.1.4", | ||||||
|     "react-immutable-proptypes": "^2.2.0", |     "react-immutable-proptypes": "^2.2.0", | ||||||
|     "react-immutable-pure-component": "^2.2.2", |     "react-immutable-pure-component": "^2.2.2", | ||||||
|  | |||||||
| @ -7,27 +7,21 @@ RSpec.describe HomeController, type: :controller do | |||||||
|     subject { get :index } |     subject { get :index } | ||||||
| 
 | 
 | ||||||
|     context 'when not signed in' do |     context 'when not signed in' do | ||||||
|       context 'when requested path is tag timeline' do |       it 'returns http success' do | ||||||
|         it 'redirects to the tag\'s permalink' do |  | ||||||
|           @request.path = '/web/timelines/tag/name' |  | ||||||
|           is_expected.to redirect_to '/tags/name' |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'redirects to about page' do |  | ||||||
|         @request.path = '/' |         @request.path = '/' | ||||||
|         is_expected.to redirect_to(about_path) |         is_expected.to have_http_status(:success) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     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 | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -9194,6 +9194,21 @@ react-event-listener@^0.6.0: | |||||||
|     prop-types "^15.6.0" |     prop-types "^15.6.0" | ||||||
|     warning "^4.0.1" |     warning "^4.0.1" | ||||||
| 
 | 
 | ||||||
|  | react-fast-compare@^3.1.1: | ||||||
|  |   version "3.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" | ||||||
|  |   integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== | ||||||
|  | 
 | ||||||
|  | react-helmet@^6.1.0: | ||||||
|  |   version "6.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" | ||||||
|  |   integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== | ||||||
|  |   dependencies: | ||||||
|  |     object-assign "^4.1.1" | ||||||
|  |     prop-types "^15.7.2" | ||||||
|  |     react-fast-compare "^3.1.1" | ||||||
|  |     react-side-effect "^2.1.0" | ||||||
|  | 
 | ||||||
| react-hotkeys@^1.1.4: | react-hotkeys@^1.1.4: | ||||||
|   version "1.1.4" |   version "1.1.4" | ||||||
|   resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72" |   resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72" | ||||||
| @ -9368,6 +9383,11 @@ react-select@^5.4.0: | |||||||
|     prop-types "^15.6.0" |     prop-types "^15.6.0" | ||||||
|     react-transition-group "^4.3.0" |     react-transition-group "^4.3.0" | ||||||
| 
 | 
 | ||||||
|  | react-side-effect@^2.1.0: | ||||||
|  |   version "2.1.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" | ||||||
|  |   integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== | ||||||
|  | 
 | ||||||
| react-sparklines@^1.7.0: | react-sparklines@^1.7.0: | ||||||
|   version "1.7.0" |   version "1.7.0" | ||||||
|   resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" |   resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user