diff options
author | Claire <claire.github-309c@sitedethib.com> | 2022-10-09 23:26:02 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-09 23:26:02 +0200 |
commit | 94713940c7f28e9aff50071cf63d897c8e355ee6 (patch) | |
tree | 109be3b0d61e50789f69704d9e1c9ffdc7e66141 /app/javascript/flavours/glitch/features | |
parent | 44486db912ac6064419680dbc3dcd3843a02a144 (diff) | |
parent | d04fbe6fe06113041662bbd9816198256d3cd385 (diff) |
Merge pull request #1861 from ClearlyClaire/glitch-soc/features/logged-out-webui
Port logged-out Web UI to glitch-soc
Diffstat (limited to 'app/javascript/flavours/glitch/features')
29 files changed, 704 insertions, 159 deletions
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index cc2a7d4d4..fc80446b1 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, me, title, domain } from 'flavours/glitch/util/initial_state'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; @@ -14,6 +14,7 @@ import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -54,6 +55,14 @@ const messages = defineMessages({ languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, }); +const titleFromAccount = account => { + const displayName = account.get('display_name'); + const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct'); + const prefix = displayName.trim().length === 0 ? account.get('username') : displayName; + + return `${prefix} (@${acct})`; +}; + const dateFormatOptions = { month: 'short', day: 'numeric', @@ -87,6 +96,7 @@ class Header extends ImmutablePureComponent { onAddToList: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, + onInteractionModal: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -124,6 +134,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl, domain } = this.props; + const { signedIn } = this.context.identity; if (!account) { return null; @@ -157,12 +168,12 @@ class Header extends ImmutablePureComponent { } if (me !== account.get('id')) { - if (!account.get('relationship')) { // Wait until the relationship is loaded + if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ''; } else if (account.getIn(['relationship', 'requested'])) { actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />; + actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />; } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; } @@ -182,7 +193,7 @@ class Header extends ImmutablePureComponent { lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; } - if (account.get('id') !== me && !suspended) { + if (signedIn && account.get('id') !== me && !suspended) { menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push(null); @@ -209,7 +220,7 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - } else { + } else if (signedIn) { if (account.getIn(['relationship', 'following'])) { if (!account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -242,7 +253,7 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } - if (account.get('acct') !== account.get('username')) { + if (signedIn && account.get('acct') !== account.get('username')) { const domain = account.get('acct').split('@')[1]; menu.push(null); @@ -301,7 +312,7 @@ class Header extends ImmutablePureComponent { </React.Fragment> )} - <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> + <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' /> </div> )} </div> @@ -313,7 +324,7 @@ class Header extends ImmutablePureComponent { </h1> </div> - <AccountNoteContainer account={account} /> + {signedIn && <AccountNoteContainer account={account} />} {!(suspended || hidden) && ( <div className='account__header__extra'> @@ -339,6 +350,10 @@ class Header extends ImmutablePureComponent { </div> )} </div> + + <Helmet> + <title>{titleFromAccount(account)} - {title}</title> + </Helmet> </div> ); } diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 783d318f9..eb332e296 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, + onInteractionModal: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -97,6 +98,10 @@ export default class Header extends ImmutablePureComponent { this.props.onChangeLanguages(this.props.account); } + handleInteractionModal = () => { + this.props.onInteractionModal(this.props.account); + } + render () { const { account, hidden, hideTabs } = this.props; @@ -124,6 +129,7 @@ export default class Header extends ImmutablePureComponent { onAddToList={this.handleAddToList} onEditAccountNote={this.handleEditAccountNote} onChangeLanguages={this.handleChangeLanguages} + onInteractionModal={this.handleInteractionModal} domain={this.props.domain} hidden={hidden} /> diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index dc8b2d55a..659dbc3fa 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -58,6 +58,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onInteractionModal (account) { + dispatch(openModal('INTERACTION', { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), + })); + }, + onBlock (account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 64030e195..b921e22e9 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -9,6 +9,8 @@ import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; +import { Helmet } from 'react-helmet'; +import { title } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -39,6 +41,7 @@ class CommunityTimeline extends React.PureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -72,18 +75,30 @@ class CommunityTimeline extends React.PureComponent { componentDidMount () { const { dispatch, onlyMedia } = this.props; + const { signedIn } = this.context.identity; dispatch(expandCommunityTimeline({ onlyMedia })); - this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + + if (signedIn) { + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } } componentDidUpdate (prevProps) { + const { signedIn } = this.context.identity; + if (prevProps.onlyMedia !== this.props.onlyMedia) { const { dispatch, onlyMedia } = this.props; - this.disconnect(); + if (this.disconnect) { + this.disconnect(); + } + dispatch(expandCommunityTimeline({ onlyMedia })); - this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + + if (signedIn) { + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } } } @@ -132,6 +147,10 @@ class CommunityTimeline extends React.PureComponent { bindToDocument={!multiColumn} regex={this.props.regex} /> + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js index 87d9b3625..1aa8a330d 100644 --- a/app/javascript/flavours/glitch/features/directory/index.js +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -13,6 +13,8 @@ import RadioButton from 'flavours/glitch/components/radio_button'; import LoadMore from 'flavours/glitch/components/load_more'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { title } from 'flavours/glitch/util/initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, @@ -165,6 +167,10 @@ class Directory extends React.PureComponent { /> {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea} + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/explore/index.js b/app/javascript/flavours/glitch/features/explore/index.js index c89463e7e..388123c38 100644 --- a/app/javascript/flavours/glitch/features/explore/index.js +++ b/app/javascript/flavours/glitch/features/explore/index.js @@ -12,6 +12,8 @@ import Suggestions from './suggestions'; import Search from 'flavours/glitch/features/compose/containers/search_container'; import SearchResults from './results'; import { showTrends } from 'flavours/glitch/util/initial_state'; +import { Helmet } from 'react-helmet'; +import { title } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ title: { id: 'explore.title', defaultMessage: 'Explore' }, @@ -29,13 +31,13 @@ class Explore extends React.PureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { intl: PropTypes.object.isRequired, multiColumn: PropTypes.bool, isSearching: PropTypes.bool, - layout: PropTypes.string, }; handleHeaderClick = () => { @@ -47,22 +49,21 @@ class Explore extends React.PureComponent { } render () { - const { intl, multiColumn, isSearching, layout } = this.props; + const { intl, multiColumn, isSearching } = this.props; + const { signedIn } = this.context.identity; return ( <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> - {layout === 'mobile' ? ( - <div className='explore__search-header'> - <Search /> - </div> - ) : ( - <ColumnHeader - icon={isSearching ? 'search' : 'hashtag'} - title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)} - onClick={this.handleHeaderClick} - multiColumn={multiColumn} - /> - )} + <ColumnHeader + icon={isSearching ? 'search' : 'hashtag'} + title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)} + onClick={this.handleHeaderClick} + multiColumn={multiColumn} + /> + + <div className='explore__search-header'> + <Search /> + </div> <div className='scrollable scrollable--flex'> {isSearching ? ( @@ -73,7 +74,7 @@ class Explore extends React.PureComponent { <NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink> <NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink> <NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink> - <NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink> + {signedIn && <NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>} </div> <Switch> @@ -82,6 +83,10 @@ class Explore extends React.PureComponent { <Route path='/explore/suggestions' component={Suggestions} /> <Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} /> </Switch> + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </React.Fragment> )} </div> diff --git a/app/javascript/flavours/glitch/features/explore/links.js b/app/javascript/flavours/glitch/features/explore/links.js index 1d8cd8efc..6adc2f6fb 100644 --- a/app/javascript/flavours/glitch/features/explore/links.js +++ b/app/javascript/flavours/glitch/features/explore/links.js @@ -5,6 +5,7 @@ import Story from './components/story'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingLinks } from 'flavours/glitch/actions/trends'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ links: state.getIn(['trends', 'links', 'items']), @@ -28,6 +29,16 @@ class Links extends React.PureComponent { render () { const { isLoading, links } = this.props; + if (!isLoading && links.isEmpty()) { + return ( + <div className='explore__links scrollable scrollable--flex'> + <div className='empty-column-indicator'> + <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> + </div> + </div> + ); + } + return ( <div className='explore__links'> {isLoading ? (<LoadingIndicator />) : links.map(link => ( diff --git a/app/javascript/flavours/glitch/features/explore/results.js b/app/javascript/flavours/glitch/features/explore/results.js index 8eac63c2c..3f43b9ee1 100644 --- a/app/javascript/flavours/glitch/features/explore/results.js +++ b/app/javascript/flavours/glitch/features/explore/results.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { expandSearch } from 'flavours/glitch/actions/search'; import Account from 'flavours/glitch/containers/account_container'; @@ -10,10 +10,17 @@ import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag' import { List as ImmutableList } from 'immutable'; import LoadMore from 'flavours/glitch/components/load_more'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { title } from 'flavours/glitch/util/initial_state'; +import { Helmet } from 'react-helmet'; + +const messages = defineMessages({ + title: { id: 'search_results.title', defaultMessage: 'Search for {q}' }, +}); const mapStateToProps = state => ({ isLoading: state.getIn(['search', 'isLoading']), results: state.getIn(['search', 'results']), + q: state.getIn(['search', 'searchTerm']), }); const appendLoadMore = (id, list, onLoadMore) => { @@ -37,6 +44,7 @@ const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', resul )), onLoadMore); export default @connect(mapStateToProps) +@injectIntl class Results extends React.PureComponent { static propTypes = { @@ -44,6 +52,8 @@ class Results extends React.PureComponent { isLoading: PropTypes.bool, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, + q: PropTypes.string, + intl: PropTypes.object, }; state = { @@ -64,7 +74,7 @@ class Results extends React.PureComponent { } render () { - const { isLoading, results } = this.props; + const { intl, isLoading, q, results } = this.props; const { type } = this.state; let filteredResults = ImmutableList(); @@ -106,6 +116,10 @@ class Results extends React.PureComponent { <div className='explore__search-results'> {isLoading ? <LoadingIndicator /> : filteredResults} </div> + + <Helmet> + <title>{intl.formatMessage(messages.title, { q })} - {title}</title> + </Helmet> </React.Fragment> ); } diff --git a/app/javascript/flavours/glitch/features/explore/suggestions.js b/app/javascript/flavours/glitch/features/explore/suggestions.js index ccd2c950a..52e5ce62b 100644 --- a/app/javascript/flavours/glitch/features/explore/suggestions.js +++ b/app/javascript/flavours/glitch/features/explore/suggestions.js @@ -5,6 +5,7 @@ import AccountCard from 'flavours/glitch/features/directory/components/account_c import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), @@ -33,6 +34,16 @@ class Suggestions extends React.PureComponent { render () { const { isLoading, suggestions } = this.props; + if (!isLoading && suggestions.isEmpty()) { + return ( + <div className='explore__suggestions scrollable scrollable--flex'> + <div className='empty-column-indicator'> + <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> + </div> + </div> + ); + } + return ( <div className='explore__suggestions'> {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => ( diff --git a/app/javascript/flavours/glitch/features/explore/tags.js b/app/javascript/flavours/glitch/features/explore/tags.js index 0ec1eb88b..465fad0df 100644 --- a/app/javascript/flavours/glitch/features/explore/tags.js +++ b/app/javascript/flavours/glitch/features/explore/tags.js @@ -5,6 +5,7 @@ import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag' import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ hashtags: state.getIn(['trends', 'tags', 'items']), @@ -28,6 +29,16 @@ class Tags extends React.PureComponent { render () { const { isLoading, hashtags } = this.props; + if (!isLoading && hashtags.isEmpty()) { + return ( + <div className='explore__links scrollable scrollable--flex'> + <div className='empty-column-indicator'> + <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> + </div> + </div> + ); + } + return ( <div className='explore__links'> {isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => ( diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.js b/app/javascript/flavours/glitch/features/follow_recommendations/index.js index d050e3cc7..f934aeb35 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/index.js +++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.js @@ -10,7 +10,6 @@ import { requestBrowserPermission } from 'flavours/glitch/actions/notifications' import { markAsPartial } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/features/ui/components/column'; import Account from './components/account'; -import Logo from 'flavours/glitch/components/logo'; import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; import Button from 'flavours/glitch/components/button'; @@ -78,7 +77,10 @@ class FollowRecommendations extends ImmutablePureComponent { <Column> <div className='scrollable follow-recommendations-container'> <div className='column-title'> - <Logo /> + <svg viewBox='0 0 79 79' className='logo'> + <use xlinkHref='#logo-symbol-icon' /> + </svg> + <h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3> <p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p> </div> diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 87a52b269..38dda85b2 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -14,6 +14,8 @@ import { isEqual } from 'lodash'; import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags'; import Icon from 'flavours/glitch/components/icon'; import classNames from 'classnames'; +import { title } from 'flavours/glitch/util/initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, @@ -31,6 +33,10 @@ class HashtagTimeline extends React.PureComponent { disconnects = []; + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { params: PropTypes.object.isRequired, columnId: PropTypes.string, @@ -90,6 +96,12 @@ class HashtagTimeline extends React.PureComponent { } _subscribe (dispatch, id, tags = {}, local) { + const { signedIn } = this.context.identity; + + if (!signedIn) { + return; + } + let any = (tags.any || []).map(tag => tag.value); let all = (tags.all || []).map(tag => tag.value); let none = (tags.none || []).map(tag => tag.value); @@ -158,6 +170,11 @@ class HashtagTimeline extends React.PureComponent { handleFollow = () => { const { dispatch, params, tag } = this.props; const { id } = params; + const { signedIn } = this.context.identity; + + if (!signedIn) { + return; + } if (tag.get('following')) { dispatch(unfollowHashtag(id)); @@ -170,6 +187,7 @@ class HashtagTimeline extends React.PureComponent { const { hasUnread, columnId, multiColumn, tag, intl } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; + const { signedIn } = this.context.identity; let followButton; @@ -177,7 +195,7 @@ class HashtagTimeline extends React.PureComponent { const following = tag.get('following'); followButton = ( - <button className={classNames('column-header__button')} onClick={this.handleFollow} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}> + <button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}> <Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' /> </button> ); @@ -208,6 +226,10 @@ class HashtagTimeline extends React.PureComponent { emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} bindToDocument={!multiColumn} /> + + <Helmet> + <title>{`#${id}`} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 51e932307..7ca28da35 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -13,6 +13,9 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; import classNames from 'classnames'; import IconWithBadge from 'flavours/glitch/components/icon_with_badge'; +import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator'; +import { Helmet } from 'react-helmet'; +import { title } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -33,6 +36,10 @@ export default @connect(mapStateToProps) @injectIntl class HomeTimeline extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -115,6 +122,7 @@ class HomeTimeline extends React.PureComponent { render () { const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; + const { signedIn } = this.context.identity; let announcementsButton = null; @@ -149,15 +157,21 @@ class HomeTimeline extends React.PureComponent { <ColumnSettingsContainer /> </ColumnHeader> - <StatusListContainer - trackScroll={!pinned} - scrollKey={`home_timeline-${columnId}`} - onLoadMore={this.handleLoadMore} - timelineId='home' - emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />} - bindToDocument={!multiColumn} - regex={this.props.regex} - /> + {signedIn ? ( + <StatusListContainer + trackScroll={!pinned} + scrollKey={`home_timeline-${columnId}`} + onLoadMore={this.handleLoadMore} + timelineId='home' + emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />} + bindToDocument={!multiColumn} + regex={this.props.regex} + /> + ) : <NotSignedInIndicator />} + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.js b/app/javascript/flavours/glitch/features/interaction_modal/index.js new file mode 100644 index 000000000..d2260c4f0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/interaction_modal/index.js @@ -0,0 +1,132 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { registrationsOpen } from 'flavours/glitch/util/initial_state'; +import { connect } from 'react-redux'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; + +const mapStateToProps = (state, { accountId }) => ({ + displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), +}); + +class Copypaste extends React.PureComponent { + + static propTypes = { + value: PropTypes.string, + }; + + state = { + copied: false, + }; + + setRef = c => { + this.input = c; + } + + handleInputClick = () => { + this.setState({ copied: false }); + this.input.focus(); + this.input.select(); + this.input.setSelectionRange(0, this.input.value.length); + } + + handleButtonClick = () => { + const { value } = this.props; + navigator.clipboard.writeText(value); + this.input.blur(); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + } + + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); + } + + render () { + const { value } = this.props; + const { copied } = this.state; + + return ( + <div className={classNames('copypaste', { copied })}> + <input + type='text' + ref={this.setRef} + value={value} + readOnly + onClick={this.handleInputClick} + /> + + <button className='button' onClick={this.handleButtonClick}> + {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />} + </button> + </div> + ); + } + +} + +export default @connect(mapStateToProps) +class InteractionModal extends React.PureComponent { + + static propTypes = { + displayNameHtml: PropTypes.string, + url: PropTypes.string, + type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']), + }; + + render () { + const { url, type, displayNameHtml } = this.props; + + const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />; + + let title, actionDescription, icon; + + switch(type) { + case 'reply': + icon = <Icon id='reply' />; + title = <FormattedMessage id='interaction_modal.title.reply' defaultMessage="Reply to {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.reply' defaultMessage='With an account on Mastodon, you can respond to this post.' />; + break; + case 'reblog': + icon = <Icon id='retweet' />; + title = <FormattedMessage id='interaction_modal.title.reblog' defaultMessage="Boost {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.reblog' defaultMessage='With an account on Mastodon, you can boost this post to share it with your own followers.' />; + break; + case 'favourite': + icon = <Icon id='star' />; + title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />; + break; + case 'follow': + icon = <Icon id='user-plus' />; + title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />; + break; + } + + return ( + <div className='modal-root__modal interaction-modal'> + <div className='interaction-modal__lead'> + <h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3> + <p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p> + </div> + + <div className='interaction-modal__choices'> + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> + <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> + <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + </div> + + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3> + <p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p> + <Copypaste value={url} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 2682dd29b..6cfafb61e 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -28,6 +28,9 @@ import LoadGap from 'flavours/glitch/components/load_gap'; import Icon from 'flavours/glitch/components/icon'; import compareId from 'flavours/glitch/util/compare_id'; import NotificationsPermissionBanner from './components/notifications_permission_banner'; +import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator'; +import { Helmet } from 'react-helmet'; +import { title } from 'flavours/glitch/util/initial_state'; import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; @@ -94,6 +97,10 @@ export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl class Notifications extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { columnId: PropTypes.string, notifications: ImmutablePropTypes.list.isRequired, @@ -224,10 +231,11 @@ class Notifications extends React.PureComponent { const { animatingNCD } = this.state; const pinned = !!columnId; const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />; + const { signedIn } = this.context.identity; let scrollableContent = null; - const filterBarContainer = showFilterBar + const filterBarContainer = (signedIn && showFilterBar) ? (<FilterBarContainer />) : null; @@ -257,26 +265,32 @@ class Notifications extends React.PureComponent { this.scrollableContent = scrollableContent; - const scrollContainer = ( - <ScrollableList - scrollKey={`notifications-${columnId}`} - trackScroll={!pinned} - isLoading={isLoading} - showLoading={isLoading && notifications.size === 0} - hasMore={hasMore} - numPending={numPending} - prepend={needsNotificationPermission && <NotificationsPermissionBanner />} - alwaysPrepend - emptyMessage={emptyMessage} - onLoadMore={this.handleLoadOlder} - onLoadPending={this.handleLoadPending} - onScrollToTop={this.handleScrollToTop} - onScroll={this.handleScroll} - bindToDocument={!multiColumn} - > - {scrollableContent} - </ScrollableList> - ); + let scrollContainer; + + if (signedIn) { + scrollContainer = ( + <ScrollableList + scrollKey={`notifications-${columnId}`} + trackScroll={!pinned} + isLoading={isLoading} + showLoading={isLoading && notifications.size === 0} + hasMore={hasMore} + numPending={numPending} + prepend={needsNotificationPermission && <NotificationsPermissionBanner />} + alwaysPrepend + emptyMessage={emptyMessage} + onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} + onScrollToTop={this.handleScrollToTop} + onScroll={this.handleScroll} + bindToDocument={!multiColumn} + > + {scrollableContent} + </ScrollableList> + ); + } else { + scrollContainer = <NotSignedInIndicator />; + } const extraButtons = []; @@ -354,8 +368,13 @@ class Notifications extends React.PureComponent { > <ColumnSettingsContainer /> </ColumnHeader> + {filterBarContainer} {scrollContainer} + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js index 0408105ae..2a5adf71d 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -44,6 +44,7 @@ class Footer extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -69,26 +70,44 @@ class Footer extends ImmutablePureComponent { }; handleReplyClick = () => { - const { dispatch, askReplyConfirmation, intl } = this.props; - - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: this._performReply, - })); + const { dispatch, askReplyConfirmation, status, intl } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + })); + } else { + this._performReply(); + } } else { - this._performReply(); + dispatch(openModal('INTERACTION', { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; handleFavouriteClick = () => { const { dispatch, status } = this.props; - - if (status.get('favourited')) { - dispatch(unfavourite(status)); + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } } else { - dispatch(favourite(status)); + dispatch(openModal('INTERACTION', { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; @@ -99,13 +118,22 @@ class Footer extends ImmutablePureComponent { handleReblogClick = e => { const { dispatch, status } = this.props; - - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else if ((e && e.shiftKey) || !boostModal) { - this._performReblog(); + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else if ((e && e.shiftKey) || !boostModal) { + this._performReblog(); + } else { + dispatch(initBoostModal({ status, onReblog: this._performReblog })); + } } else { - dispatch(initBoostModal({ status, onReblog: this._performReblog })); + dispatch(openModal('INTERACTION', { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 9f31cf922..6e1fb8ced 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -9,6 +9,8 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; import { connectPublicStream } from 'flavours/glitch/actions/streaming'; +import { Helmet } from 'react-helmet'; +import { title } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, @@ -43,6 +45,7 @@ class PublicTimeline extends React.PureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -78,18 +81,29 @@ class PublicTimeline extends React.PureComponent { componentDidMount () { const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; + const { signedIn } = this.context.identity; dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); + if (signedIn) { + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); + } } componentDidUpdate (prevProps) { + const { signedIn } = this.context.identity; + if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) { const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; - this.disconnect(); + if (this.disconnect) { + this.disconnect(); + } + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); + + if (signedIn) { + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); + } } } @@ -138,6 +152,10 @@ class PublicTimeline extends React.PureComponent { bindToDocument={!multiColumn} regex={this.props.regex} /> + + <Helmet> + <title>{intl.formatMessage(messages.title)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js index bea21b1b7..43fb7a17c 100644 --- a/app/javascript/flavours/glitch/features/report/category.js +++ b/app/javascript/flavours/glitch/features/report/category.js @@ -20,7 +20,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - rules: state.get('rules'), + rules: state.getIn(['server', 'rules']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/features/report/rules.js b/app/javascript/flavours/glitch/features/report/rules.js index 4772e04a2..599c04dbd 100644 --- a/app/javascript/flavours/glitch/features/report/rules.js +++ b/app/javascript/flavours/glitch/features/report/rules.js @@ -7,7 +7,7 @@ import Button from 'flavours/glitch/components/button'; import Option from './components/option'; const mapStateToProps = state => ({ - rules: state.get('rules'), + rules: state.getIn(['server', 'rules']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index ef0f0f2b7..75ad462f0 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -152,6 +152,7 @@ class ActionBar extends React.PureComponent { render () { const { status, intl } = this.props; + const { signedIn, permissions } = this.context.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); @@ -184,7 +185,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { menu.push(null); if (accountAdminLink !== undefined) { menu.push({ @@ -224,7 +225,7 @@ class ActionBar extends React.PureComponent { <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> {shareButton} - <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> + <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__action-bar-dropdown'> <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title={intl.formatMessage(messages.more)} /> diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 9c86d54db..8e783af80 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -47,11 +47,12 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; -import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; +import { boostModal, favouriteModal, deleteModal, title } from 'flavours/glitch/util/initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status'; import Icon from 'flavours/glitch/components/icon'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -147,12 +148,30 @@ const makeMapStateToProps = () => { return mapStateToProps; }; +const truncate = (str, num) => { + if (str.length > num) { + return str.slice(0, num) + '…'; + } else { + return str; + } +}; + +const titleFromStatus = status => { + const displayName = status.getIn(['account', 'display_name']); + const username = status.getIn(['account', 'username']); + const prefix = displayName.trim().length === 0 ? username : displayName; + const text = status.get('search_index'); + + return `${prefix}: "${truncate(text, 30)}"`; +}; + export default @injectIntl @connect(makeMapStateToProps) class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -245,14 +264,25 @@ class Status extends ImmutablePureComponent { } handleFavouriteClick = (status, e) => { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); - } else { - if ((e && e.shiftKey) || !favouriteModal) { - this.handleModalFavourite(status); + const { dispatch } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); } else { - this.props.dispatch(openModal('FAVOURITE', { status, onFavourite: this.handleModalFavourite })); + if ((e && e.shiftKey) || !favouriteModal) { + this.handleModalFavourite(status); + } else { + dispatch(openModal('FAVOURITE', { status, onFavourite: this.handleModalFavourite })); + } } + } else { + dispatch(openModal('INTERACTION', { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } @@ -265,16 +295,26 @@ class Status extends ImmutablePureComponent { } handleReplyClick = (status) => { - let { askReplyConfirmation, dispatch, intl } = this.props; - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), - onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), - })); + const { askReplyConfirmation, dispatch, intl } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), + })); + } else { + dispatch(replyCompose(status, this.context.router.history)); + } } else { - dispatch(replyCompose(status, this.context.router.history)); + dispatch(openModal('INTERACTION', { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } @@ -290,13 +330,22 @@ class Status extends ImmutablePureComponent { handleReblogClick = (status, e) => { const { settings, dispatch } = this.props; + const { signedIn } = this.context.identity; - if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) { - dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true })); - } else if ((e && e.shiftKey) || !boostModal) { - this.handleModalReblog(status); + if (signedIn) { + if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) { + dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true })); + } else if ((e && e.shiftKey) || !boostModal) { + this.handleModalReblog(status); + } else { + dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); + } } else { - dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); + dispatch(openModal('INTERACTION', { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } @@ -633,6 +682,10 @@ class Status extends ImmutablePureComponent { {descendants} </div> </ScrollContainer> + + <Helmet> + <title>{titleFromStatus(status)} - {title}</title> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 048251fa6..5f5018105 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -60,6 +60,7 @@ class ColumnsArea extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, }; static propTypes = { @@ -213,11 +214,12 @@ class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props; const { shouldAnimate, renderComposePanel } = this.state; + const { signedIn } = this.context.identity; const columnIndex = getIndex(this.context.router.history.location.pathname); if (singleColumn) { - const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; + const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const content = columnIndex !== -1 ? ( <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}> diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js index 498f09ab6..6e1c51d74 100644 --- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js @@ -1,16 +1,42 @@ import React from 'react'; +import PropTypes from 'prop-types'; import SearchContainer from 'flavours/glitch/features/compose/containers/search_container'; import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container'; import LinkFooter from './link_footer'; +import ServerBanner from 'flavours/glitch/components/server_banner'; -const ComposePanel = () => ( - <div className='compose-panel'> - <SearchContainer openInRoute /> - <NavigationContainer /> - <ComposeFormContainer singleColumn /> - <LinkFooter withHotkeys /> - </div> -); +export default +class ComposePanel extends React.PureComponent { -export default ComposePanel; + static contextTypes = { + identity: PropTypes.object.isRequired, + }; + + render() { + const { signedIn } = this.context.identity; + + return ( + <div className='compose-panel'> + <SearchContainer openInRoute /> + + {!signedIn && ( + <React.Fragment> + <ServerBanner /> + <div className='flex-spacer' /> + </React.Fragment> + )} + + {signedIn && ( + <React.Fragment> + <NavigationContainer /> + <ComposeFormContainer singleColumn /> + </React.Fragment> + )} + + <LinkFooter withHotkeys /> + </div> + ); + } + +}; diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index 67fa067f9..6f4a8c2de 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -34,6 +34,7 @@ class LinkFooter extends React.PureComponent { }; static propTypes = { + withHotkeys: PropTypes.bool, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -48,18 +49,53 @@ class LinkFooter extends React.PureComponent { } render () { + const { withHotkeys } = this.props; + const { signedIn, permissions } = this.context.identity; + + const items = []; + + if ((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) { + items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>); + } + + if (signedIn && withHotkeys) { + items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>); + } + + if (signedIn && securityLink) { + items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>); + } + + if (!limitedFederationMode) { + items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a>); + } + + if (profileDirectory) { + items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link>); + } + + items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a>); + + if (privacyPolicyLink) { + items.push(<a key='terms' href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>); + } + + if (signedIn) { + items.push(<a key='developers' href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a>); + } + + items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>); + + if (signedIn) { + items.push(<a key='logout' href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>); + } + return ( <div className='getting-started__footer'> <ul> - {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} - {!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>} - {!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>} - {profileDirectory && <li><Link to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link> · </li>} - <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> - <li><a href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a> · </li> - <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> - <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> - <li><a href={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> + {items.map((item, index, array) => ( + <li>{item} { index === array.length - 1 ? null : ' · ' }</li> + ))} </ul> <p> diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index 7e8082f99..92768caeb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -16,6 +16,7 @@ import ConfirmationModal from './confirmation_modal'; import SubscribedLanguagesModal from 'flavours/glitch/features/subscribed_languages_modal'; import FocalPointModal from './focal_point_modal'; import DeprecatedSettingsModal from './deprecated_settings_modal'; +import InteractionModal from 'flavours/glitch/features/interaction_modal'; import { OnboardingModal, MuteModal, @@ -53,6 +54,7 @@ const MODAL_COMPONENTS = { 'COMPARE_HISTORY': CompareHistoryModal, 'FILTER': FilterModal, 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }), + 'INTERACTION': () => Promise.resolve({ default: InteractionModal }), }; export default class ModalRoot extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js index f4d649456..453276775 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js @@ -1,5 +1,6 @@ import React from 'react'; -import { NavLink, withRouter } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { NavLink, Link } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; import { showTrends } from 'flavours/glitch/util/initial_state'; @@ -8,30 +9,70 @@ import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; import TrendsContainer from 'flavours/glitch/features/getting_started/containers/trends_container'; +import SignInBanner from './sign_in_banner'; -const NavigationPanel = ({ onOpenSettings }) => ( - <div className='navigation-panel'> - <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 /> - { showTrends && <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink> } - <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> - <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> +export default class NavigationPanel extends React.Component { - <ListPanel /> + static contextTypes = { + router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, + }; - <hr /> + static propTypes = { + onOpenSettings: PropTypes.func, + }; - {!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>} - <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' id='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a> - {!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>} + render() { + const { signedIn } = this.context.identity; + const { onOpenSettings } = this.props; - {showTrends && <div className='flex-spacer' />} - {showTrends && <TrendsContainer />} - </div> -); + return ( + <div className='navigation-panel'> + {signedIn && ( + <React.Fragment> + <NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> + <FollowRequestsNavLink /> + </React.Fragment> + )} -export default withRouter(NavigationPanel); + { showTrends && <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink> } + + <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> + <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> + + {!signedIn && ( + <React.Fragment> + <hr /> + <SignInBanner /> + </React.Fragment> + )} + + {signedIn && ( + <React.Fragment> + <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> + + <ListPanel /> + + <hr /> + + {!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>} + <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' id='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a> + {!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>} + </React.Fragment> + )} + + {showTrends && ( + <React.Fragment> + <div className='flex-spacer' /> + <TrendsContainer /> + </React.Fragment> + )} + + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index dcbe94929..7b6a4a784 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { submitReport } from 'flavours/glitch/actions/reports'; import { expandAccountTimeline } from 'flavours/glitch/actions/timelines'; -import { fetchRules } from 'flavours/glitch/actions/rules'; +import { fetchServer } from 'flavours/glitch/actions/server'; import { fetchRelationships } from 'flavours/glitch/actions/accounts'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -119,7 +119,7 @@ class ReportModal extends ImmutablePureComponent { dispatch(fetchRelationships([accountId])); dispatch(expandAccountTimeline(accountId, { withReplies: true })); - dispatch(fetchRules()); + dispatch(fetchServer()); } render () { diff --git a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js new file mode 100644 index 000000000..e08a1ea67 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { registrationsOpen } from 'flavours/glitch/util/initial_state'; + +const SignInBanner = () => ( + <div className='sign-in-banner'> + <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p> + <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> + <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + </div> +); + +export default SignInBanner; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 7b547fd5b..735623e3d 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -10,7 +10,7 @@ import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; -import { fetchRules } from 'flavours/glitch/actions/rules'; +import { fetchServer } from 'flavours/glitch/actions/server'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; @@ -54,9 +54,10 @@ import { FollowRecommendations, } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, title } from 'flavours/glitch/util/initial_state'; import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { Helmet } from 'react-helmet'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -121,6 +122,10 @@ const keyMap = { class SwitchingColumnsArea extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { children: PropTypes.node, location: PropTypes.object, @@ -157,12 +162,25 @@ class SwitchingColumnsArea extends React.PureComponent { render () { const { children, mobile, navbarUnder } = this.props; - const redirect = mobile ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />; + const { signedIn } = this.context.identity; + + let redirect; + + if (signedIn) { + if (mobile) { + redirect = <Redirect from='/' to='/home' exact />; + } else { + redirect = <Redirect from='/' to='/getting-started' exact />; + } + } else { + redirect = <Redirect from='/' to='/explore' exact />; + } return ( <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile} navbarUnder={navbarUnder}> <WrappedSwitch> {redirect} + <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> @@ -219,6 +237,10 @@ export default @connect(mapStateToProps) @withRouter class UI extends React.Component { + static contextTypes = { + identity: PropTypes.object.isRequired, + }; + static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, @@ -358,6 +380,8 @@ class UI extends React.Component { } componentDidMount () { + const { signedIn } = this.context.identity; + window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); @@ -374,16 +398,18 @@ class UI extends React.Component { this.favicon = new Favico({ animation:"none" }); // On first launch, redirect to the follow recommendations page - if (this.props.firstLaunch) { + if (signedIn && this.props.firstLaunch) { this.context.router.history.replace('/start'); this.props.dispatch(closeOnboarding()); } - this.props.dispatch(fetchMarkers()); - this.props.dispatch(expandHomeTimeline()); - this.props.dispatch(expandNotifications()); + if (signedIn) { + this.props.dispatch(fetchMarkers()); + this.props.dispatch(expandHomeTimeline()); + this.props.dispatch(expandNotifications()); - setTimeout(() => this.props.dispatch(fetchRules()), 3000); + setTimeout(() => this.props.dispatch(fetchServer()), 3000); + } this.hotkeys.__mousetrap__.stopCallback = (e, element) => { return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); @@ -635,6 +661,10 @@ class UI extends React.Component { <LoadingBarContainer className='loading-bar' /> <ModalContainer /> <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> + + <Helmet> + <title>{title}</title> + </Helmet> </div> </HotKeys> ); |