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 | ||||
|   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 | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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 accessToken = getState().getIn(['meta', 'access_token'], ''); | ||||
|   const params      = _buildParams(getState()); | ||||
| 
 | ||||
|   if (Object.keys(params).length === 0) { | ||||
|   if (Object.keys(params).length === 0 || accessToken === '') { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,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> | ||||
| ); | ||||
| 
 | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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 => ( | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -5,6 +5,7 @@ import AccountCard from 'mastodon/features/directory/components/account_card'; | ||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { fetchSuggestions } from 'mastodon/actions/suggestions'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   suggestions: state.getIn(['suggestions', 'items']), | ||||
| @ -28,6 +29,16 @@ class Suggestions extends React.PureComponent { | ||||
|   render () { | ||||
|     const { isLoading, suggestions } = this.props; | ||||
| 
 | ||||
|     if (!isLoading && suggestions.isEmpty()) { | ||||
|       return ( | ||||
|         <div className='explore__suggestions scrollable scrollable--flex'> | ||||
|           <div className='empty-column-indicator'> | ||||
|             <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> | ||||
|           </div> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='explore__suggestions'> | ||||
|         {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => ( | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; | ||||
| import LoadingIndicator from 'mastodon/components/loading_indicator'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { fetchTrendingHashtags } from 'mastodon/actions/trends'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   hashtags: state.getIn(['trends', 'tags', 'items']), | ||||
| @ -28,6 +29,16 @@ class Tags extends React.PureComponent { | ||||
|   render () { | ||||
|     const { isLoading, hashtags } = this.props; | ||||
| 
 | ||||
|     if (!isLoading && hashtags.isEmpty()) { | ||||
|       return ( | ||||
|         <div className='explore__links scrollable scrollable--flex'> | ||||
|           <div className='empty-column-indicator'> | ||||
|             <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> | ||||
|           </div> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='explore__links'> | ||||
|         {isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => ( | ||||
|  | ||||
| @ -14,6 +14,8 @@ import { isEqual } from 'lodash'; | ||||
| import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import classNames from 'classnames'; | ||||
| import { title } from 'mastodon/initial_state'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, | ||||
| @ -31,6 +33,10 @@ class HashtagTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   disconnects = []; | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     identity: PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     params: PropTypes.object.isRequired, | ||||
|     columnId: PropTypes.string, | ||||
| @ -158,6 +164,11 @@ class HashtagTimeline extends React.PureComponent { | ||||
|   handleFollow = () => { | ||||
|     const { dispatch, params, tag } = this.props; | ||||
|     const { id } = params; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (!signedIn) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (tag.get('following')) { | ||||
|       dispatch(unfollowHashtag(id)); | ||||
| @ -170,6 +181,7 @@ class HashtagTimeline extends React.PureComponent { | ||||
|     const { hasUnread, columnId, multiColumn, tag, intl } = this.props; | ||||
|     const { id, local } = this.props.params; | ||||
|     const pinned = !!columnId; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     let followButton; | ||||
| 
 | ||||
| @ -177,7 +189,7 @@ class HashtagTimeline extends React.PureComponent { | ||||
|       const following = tag.get('following'); | ||||
| 
 | ||||
|       followButton = ( | ||||
|         <button className={classNames('column-header__button')} onClick={this.handleFollow} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}> | ||||
|         <button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}> | ||||
|           <Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' /> | ||||
|         </button> | ||||
|       ); | ||||
| @ -208,6 +220,10 @@ class HashtagTimeline extends React.PureComponent { | ||||
|           emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} | ||||
|           bindToDocument={!multiColumn} | ||||
|         /> | ||||
| 
 | ||||
|         <Helmet> | ||||
|           <title>{`#${id}`} - {title}</title> | ||||
|         </Helmet> | ||||
|       </Column> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -9,6 +9,8 @@ import { expandPublicTimeline } from '../../actions/timelines'; | ||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | ||||
| import { connectPublicStream } from '../../actions/streaming'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| import { title } from 'mastodon/initial_state'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   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' />} | ||||
|           bindToDocument={!multiColumn} | ||||
|         /> | ||||
| 
 | ||||
|         <Helmet> | ||||
|           <title>{intl.formatMessage(messages.title)} - {title}</title> | ||||
|         </Helmet> | ||||
|       </Column> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -56,10 +56,11 @@ import { openModal } from '../../actions/modal'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| 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 { textForScreenReader, defaultMediaVisibility } from '../../components/status'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, | ||||
| @ -156,6 +157,23 @@ const makeMapStateToProps = () => { | ||||
|   return mapStateToProps; | ||||
| }; | ||||
| 
 | ||||
| const truncate = (str, num) => { | ||||
|   if (str.length > num) { | ||||
|     return str.slice(0, num) + '…'; | ||||
|   } else { | ||||
|     return str; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const titleFromStatus = status => { | ||||
|   const displayName = status.getIn(['account', 'display_name']); | ||||
|   const username = status.getIn(['account', 'username']); | ||||
|   const prefix = displayName.trim().length === 0 ? username : displayName; | ||||
|   const text = status.get('search_index'); | ||||
| 
 | ||||
|   return `${prefix}: "${truncate(text, 30)}"`; | ||||
| }; | ||||
| 
 | ||||
| export default @injectIntl | ||||
| @connect(makeMapStateToProps) | ||||
| class Status extends ImmutablePureComponent { | ||||
| @ -605,6 +623,10 @@ class Status extends ImmutablePureComponent { | ||||
|             {descendants} | ||||
|           </div> | ||||
|         </ScrollContainer> | ||||
| 
 | ||||
|         <Helmet> | ||||
|           <title>{titleFromStatus(status)} - {title}</title> | ||||
|         </Helmet> | ||||
|       </Column> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -60,6 +60,7 @@ class ColumnsArea extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object.isRequired, | ||||
|     identity: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
| @ -212,11 +213,12 @@ class ColumnsArea extends ImmutablePureComponent { | ||||
|   render () { | ||||
|     const { columns, children, singleColumn, isModalOpen, intl } = this.props; | ||||
|     const { shouldAnimate, renderComposePanel } = this.state; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     const columnIndex = getIndex(this.context.router.history.location.pathname); | ||||
| 
 | ||||
|     if (singleColumn) { | ||||
|       const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; | ||||
|       const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; | ||||
| 
 | ||||
|       const content = columnIndex !== -1 ? ( | ||||
|         <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}> | ||||
|  | ||||
| @ -10,6 +10,10 @@ import { changeComposing } from 'mastodon/actions/compose'; | ||||
| export default @connect() | ||||
| class ComposePanel extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     identity: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|   }; | ||||
| @ -23,11 +27,25 @@ class ComposePanel extends React.PureComponent { | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='compose-panel' onFocus={this.onFocus}> | ||||
|         <SearchContainer openInRoute /> | ||||
| 
 | ||||
|         {!signedIn && ( | ||||
|           <React.Fragment> | ||||
|             <div className='flex-spacer' /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         {signedIn && ( | ||||
|           <React.Fragment> | ||||
|             <NavigationContainer onClose={this.onBlur} /> | ||||
|             <ComposeFormContainer singleColumn /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         <LinkFooter withHotkeys /> | ||||
|       </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 () { | ||||
|     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 ( | ||||
|       <div className='getting-started__footer'> | ||||
|         <ul> | ||||
|           {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} | ||||
|           {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> | ||||
|           <li>{items.reduce((prev, curr) => [prev, ' · ', curr])}</li> | ||||
|         </ul> | ||||
| 
 | ||||
|         <p> | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import React from 'react'; | ||||
| import { NavLink, withRouter } from 'react-router-dom'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { NavLink, Link } from 'react-router-dom'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import { showTrends } from 'mastodon/initial_state'; | ||||
| @ -7,15 +8,46 @@ import NotificationsCounterIcon from './notifications_counter_icon'; | ||||
| import FollowRequestsNavLink from './follow_requests_nav_link'; | ||||
| import ListPanel from './list_panel'; | ||||
| 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'> | ||||
|         <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='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> | ||||
|             <FollowRequestsNavLink /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink> | ||||
|         <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> | ||||
|         <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> | ||||
| 
 | ||||
|         {!signedIn && ( | ||||
|           <React.Fragment> | ||||
|             <hr /> | ||||
|             <SignInBanner /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         {signedIn && ( | ||||
|           <React.Fragment> | ||||
|             <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='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='/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='/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 && <TrendsContainer />} | ||||
|         {showTrends && ( | ||||
|           <React.Fragment> | ||||
|             <div className='flex-spacer' /> | ||||
|             <TrendsContainer /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
|       </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 UploadArea from './components/upload_area'; | ||||
| import ColumnsAreaContainer from './containers/columns_area_container'; | ||||
| import DocumentTitle from './components/document_title'; | ||||
| import PictureInPicture from 'mastodon/features/picture_in_picture'; | ||||
| import { | ||||
|   Compose, | ||||
| @ -53,8 +52,9 @@ import { | ||||
|   Explore, | ||||
|   FollowRecommendations, | ||||
| } from './util/async-components'; | ||||
| import { me } from '../../initial_state'; | ||||
| import { me, title } from '../../initial_state'; | ||||
| 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.
 | ||||
| // Without this it ends up in ~8 very commonly used bundles.
 | ||||
| @ -110,6 +110,10 @@ const keyMap = { | ||||
| 
 | ||||
| class SwitchingColumnsArea extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     identity: PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     children: PropTypes.node, | ||||
|     location: PropTypes.object, | ||||
| @ -145,12 +149,25 @@ class SwitchingColumnsArea extends React.PureComponent { | ||||
| 
 | ||||
|   render () { | ||||
|     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 ( | ||||
|       <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> | ||||
|         <WrappedSwitch> | ||||
|           {redirect} | ||||
| 
 | ||||
|           <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> | ||||
|           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> | ||||
| 
 | ||||
| @ -208,6 +225,7 @@ class UI extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object.isRequired, | ||||
|     identity: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
| @ -343,6 +361,8 @@ class UI extends React.PureComponent { | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     window.addEventListener('focus', this.handleWindowFocus, false); | ||||
|     window.addEventListener('blur', this.handleWindowBlur, 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
 | ||||
|     if (this.props.firstLaunch) { | ||||
|     if (signedIn && this.props.firstLaunch) { | ||||
|       this.context.router.history.replace('/start'); | ||||
|       this.props.dispatch(closeOnboarding()); | ||||
|     } | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       this.props.dispatch(fetchMarkers()); | ||||
|       this.props.dispatch(expandHomeTimeline()); | ||||
|       this.props.dispatch(expandNotifications()); | ||||
| 
 | ||||
|       setTimeout(() => this.props.dispatch(fetchRules()), 3000); | ||||
|     } | ||||
| 
 | ||||
|     this.hotkeys.__mousetrap__.stopCallback = (e, element) => { | ||||
|       return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); | ||||
| @ -546,7 +568,10 @@ class UI extends React.PureComponent { | ||||
|           <LoadingBarContainer className='loading-bar' /> | ||||
|           <ModalContainer /> | ||||
|           <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> | ||||
|           <DocumentTitle /> | ||||
| 
 | ||||
|           <Helmet> | ||||
|             <title>{title}</title> | ||||
|           </Helmet> | ||||
|         </div> | ||||
|       </HotKeys> | ||||
|     ); | ||||
|  | ||||
| @ -3,6 +3,7 @@ const initialState = element && JSON.parse(element.textContent); | ||||
| 
 | ||||
| const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; | ||||
| 
 | ||||
| export const domain = getMeta('domain'); | ||||
| export const reduceMotion = getMeta('reduce_motion'); | ||||
| export const autoPlayGif = getMeta('auto_play_gif'); | ||||
| export const displayMedia = getMeta('display_media'); | ||||
| @ -26,5 +27,6 @@ export const title = getMeta('title'); | ||||
| export const cropImages = getMeta('crop_images'); | ||||
| export const disableSwiping = getMeta('disable_swiping'); | ||||
| export const languages = initialState && initialState.languages; | ||||
| export const server = initialState && initialState.server; | ||||
| 
 | ||||
| export default initialState; | ||||
|  | ||||
| @ -20,6 +20,7 @@ | ||||
|   font-family: inherit; | ||||
|   background: $ui-base-color; | ||||
|   color: $darker-text-color; | ||||
|   border-radius: 4px; | ||||
|   font-size: 14px; | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| @ -126,6 +126,7 @@ | ||||
|     &:hover { | ||||
|       border-color: lighten($ui-primary-color, 4%); | ||||
|       color: lighten($darker-text-color, 4%); | ||||
|       text-decoration: none; | ||||
|     } | ||||
| 
 | ||||
|     &:disabled { | ||||
| @ -700,6 +701,15 @@ | ||||
|   transition: height 0.4s ease, opacity 0.4s ease; | ||||
| } | ||||
| 
 | ||||
| .sign-in-banner { | ||||
|   padding: 10px; | ||||
| 
 | ||||
|   p { | ||||
|     color: $darker-text-color; | ||||
|     margin-bottom: 20px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .emojione { | ||||
|   font-size: inherit; | ||||
|   vertical-align: middle; | ||||
| @ -2214,6 +2224,7 @@ a.account__display-name { | ||||
| 
 | ||||
|   > .scrollable { | ||||
|     background: $ui-base-color; | ||||
|     border-radius: 0 0 4px 4px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -2660,6 +2671,26 @@ a.account__display-name { | ||||
|   height: calc(100% - 10px); | ||||
|   overflow-y: hidden; | ||||
| 
 | ||||
|   .hero-widget { | ||||
|     box-shadow: none; | ||||
| 
 | ||||
|     &__text, | ||||
|     &__img, | ||||
|     &__img img { | ||||
|       border-radius: 0; | ||||
|     } | ||||
| 
 | ||||
|     &__text { | ||||
|       padding: 15px; | ||||
|       color: $secondary-text-color; | ||||
| 
 | ||||
|       strong { | ||||
|         font-weight: 700; | ||||
|         color: $primary-text-color; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .navigation-bar { | ||||
|     padding-top: 20px; | ||||
|     padding-bottom: 20px; | ||||
| @ -2667,10 +2698,6 @@ a.account__display-name { | ||||
|     min-height: 20px; | ||||
|   } | ||||
| 
 | ||||
|   .flex-spacer { | ||||
|     background: transparent; | ||||
|   } | ||||
| 
 | ||||
|   .compose-form { | ||||
|     flex: 1; | ||||
|     overflow-y: hidden; | ||||
| @ -2709,6 +2736,14 @@ a.account__display-name { | ||||
|     flex: 0 0 auto; | ||||
|   } | ||||
| 
 | ||||
|   .logo { | ||||
|     height: 30px; | ||||
|     width: auto; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .navigation-panel, | ||||
| .compose-panel { | ||||
|   hr { | ||||
|     flex: 0 0 auto; | ||||
|     border: 0; | ||||
| @ -2836,6 +2871,7 @@ a.account__display-name { | ||||
|   box-sizing: border-box; | ||||
|   width: 100%; | ||||
|   background: lighten($ui-base-color, 4%); | ||||
|   border-radius: 4px 4px 0 0; | ||||
|   color: $highlight-text-color; | ||||
|   cursor: pointer; | ||||
|   flex: 0 0 auto; | ||||
| @ -3031,6 +3067,17 @@ a.account__display-name { | ||||
|       color: $highlight-text-color; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &--logo { | ||||
|     background: transparent; | ||||
|     padding: 10px; | ||||
| 
 | ||||
|     &:hover, | ||||
|     &:focus, | ||||
|     &:active { | ||||
|       background: transparent; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .column-link__icon { | ||||
| @ -3551,6 +3598,7 @@ a.status-card.compact:hover { | ||||
|   display: flex; | ||||
|   font-size: 16px; | ||||
|   background: lighten($ui-base-color, 4%); | ||||
|   border-radius: 4px 4px 0 0; | ||||
|   flex: 0 0 auto; | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|  | ||||
| @ -17,10 +17,6 @@ class PermalinkRedirector | ||||
|         find_status_url_by_id(path_segments[2]) | ||||
|       elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/ | ||||
|         find_account_url_by_id(path_segments[2]) | ||||
|       elsif path_segments[1] == 'timelines' && path_segments[2] == 'tag' && path_segments[3].present? | ||||
|         find_tag_url_by_name(path_segments[3]) | ||||
|       elsif path_segments[1] == 'tags' && path_segments[2].present? | ||||
|         find_tag_url_by_name(path_segments[2]) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class InitialStateSerializer < ActiveModel::Serializer | ||||
|   include RoutingHelper | ||||
| 
 | ||||
|   attributes :meta, :compose, :accounts, | ||||
|              :media_attachments, :settings, | ||||
|              :languages | ||||
|              :languages, :server | ||||
| 
 | ||||
|   has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer | ||||
|   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]] } | ||||
|   end | ||||
| 
 | ||||
|   def server | ||||
|     { | ||||
|       hero: instance_presenter.hero&.file&.url || instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), | ||||
|       description: instance_presenter.site_short_description.presence || I18n.t('about.about_mastodon_html'), | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def instance_presenter | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| - content_for :header_tags do | ||||
|   - if user_signed_in? | ||||
|     = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous' | ||||
|     = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous' | ||||
|     = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous' | ||||
|     = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous' | ||||
| 
 | ||||
|   = render partial: 'shared/og' | ||||
| 
 | ||||
|   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} | ||||
| 
 | ||||
|   = render_initial_state | ||||
|   = javascript_pack_tag 'application', crossorigin: 'anonymous' | ||||
| 
 | ||||
|  | ||||
| @ -92,6 +92,7 @@ | ||||
|     "punycode": "^2.1.0", | ||||
|     "react": "^16.14.0", | ||||
|     "react-dom": "^16.14.0", | ||||
|     "react-helmet": "^6.1.0", | ||||
|     "react-hotkeys": "^1.1.4", | ||||
|     "react-immutable-proptypes": "^2.2.0", | ||||
|     "react-immutable-pure-component": "^2.2.2", | ||||
|  | ||||
| @ -7,27 +7,21 @@ RSpec.describe HomeController, type: :controller do | ||||
|     subject { get :index } | ||||
| 
 | ||||
|     context 'when not signed in' do | ||||
|       context 'when requested path is tag timeline' 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 | ||||
|       it 'returns http success' do | ||||
|         @request.path = '/' | ||||
|         is_expected.to redirect_to(about_path) | ||||
|         is_expected.to have_http_status(:success) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when signed in' do | ||||
|       let(:user) { Fabricate(:user) } | ||||
| 
 | ||||
|       before { sign_in(user) } | ||||
|       before do | ||||
|         sign_in(user) | ||||
|       end | ||||
| 
 | ||||
|       it 'assigns @body_classes' do | ||||
|         subject | ||||
|         expect(assigns(:body_classes)).to eq 'app-body' | ||||
|       it 'returns http success' do | ||||
|         is_expected.to have_http_status(:success) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -21,7 +21,7 @@ describe PermalinkRedirector do | ||||
| 
 | ||||
|     it 'returns path for legacy tag links' do | ||||
|       redirector = described_class.new('web/timelines/tag/hoge') | ||||
|       expect(redirector.redirect_path).to eq '/tags/hoge' | ||||
|       expect(redirector.redirect_path).to be_nil | ||||
|     end | ||||
| 
 | ||||
|     it 'returns path for pretty account links' do | ||||
| @ -36,7 +36,7 @@ describe PermalinkRedirector do | ||||
| 
 | ||||
|     it 'returns path for pretty tag links' do | ||||
|       redirector = described_class.new('web/tags/hoge') | ||||
|       expect(redirector.redirect_path).to eq '/tags/hoge' | ||||
|       expect(redirector.redirect_path).to be_nil | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										20
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -9194,6 +9194,21 @@ react-event-listener@^0.6.0: | ||||
|     prop-types "^15.6.0" | ||||
|     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: | ||||
|   version "1.1.4" | ||||
|   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" | ||||
|     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: | ||||
|   version "1.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user