diff options
Diffstat (limited to 'app/javascript/mastodon/features')
32 files changed, 1050 insertions, 112 deletions
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 07a6c5dec..57678d162 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/account/header + import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index edfedb864..c3cd4e55d 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -1,7 +1,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import InnerHeader from '../../account/components/header'; +import InnerHeader from '../../../../glitch/components/account/header'; import ActionBar from '../../account/components/action_bar'; import MissingIndicator from '../../../components/missing_indicator'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index ab75b40de..9ad13a231 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -7,10 +7,10 @@ import { unfollowAccount, blockAccount, unblockAccount, - muteAccount, unmuteAccount, } from '../../../actions/accounts'; import { mentionCompose } from '../../../actions/compose'; +import { initMuteModal } from '../../../actions/mutes'; import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; @@ -19,7 +19,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); @@ -77,11 +76,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))), - })); + dispatch(initMuteModal(account)); } }, diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index fe92216d5..e3b864aee 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -61,7 +61,7 @@ export default class AccountTimeline extends ImmutablePureComponent { } return ( - <Column> + <Column name='account'> <ColumnBackButton /> <StatusList diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index b16af4b28..e73d984a9 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -54,7 +54,7 @@ export default class Blocks extends ImmutablePureComponent { } return ( - <Column icon='ban' heading={intl.formatMessage(messages.heading)}> + <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollContainer scrollKey='blocks'> <div className='scrollable' onScroll={this.handleScroll}> diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 596a89412..62b1c8ee9 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -79,7 +79,7 @@ export default class CommunityTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef}> + <Column ref={this.setRef} name='local'> <ColumnHeader icon='users' active={hasUnread} diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 7d175a912..2da656fc0 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -5,11 +5,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; import Collapsable from '../../../components/collapsable'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; +import ComposeAdvancedOptionsContainer from '../../../../glitch/components/compose/advanced_options/container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import UploadFormContainer from '../containers/upload_form_container'; @@ -18,6 +18,7 @@ import { isMobile } from '../../../is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; import { countableText } from '../util/counter'; +import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -36,6 +37,9 @@ export default class ComposeForm extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list, spoiler: PropTypes.bool, privacy: PropTypes.string, + advanced_options: ImmutablePropTypes.contains({ + do_not_federate: PropTypes.bool, + }), spoiler_text: PropTypes.string, focusDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date), @@ -46,11 +50,13 @@ export default class ComposeForm extends ImmutablePureComponent { onSubmit: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired, onFetchSuggestions: PropTypes.func.isRequired, + onPrivacyChange: PropTypes.func.isRequired, onSuggestionSelected: PropTypes.func.isRequired, onChangeSpoilerText: PropTypes.func.isRequired, onPaste: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired, showSearch: PropTypes.bool, + settings : ImmutablePropTypes.map.isRequired, }; static defaultProps = { @@ -67,6 +73,11 @@ export default class ComposeForm extends ImmutablePureComponent { } } + handleSubmit2 = () => { + this.props.onPrivacyChange(this.props.settings.get('side_arm')); + this.handleSubmit(); + } + handleSubmit = () => { if (this.props.text !== this.autosuggestTextarea.textarea.value) { // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) @@ -145,16 +156,58 @@ export default class ComposeForm extends ImmutablePureComponent { render () { const { intl, onPaste, showSearch } = this.props; const disabled = this.props.is_submitting; - const text = [this.props.spoiler_text, countableText(this.props.text)].join(''); + const maybeEye = (this.props.advanced_options && this.props.advanced_options.do_not_federate) ? ' 👁️' : ''; + const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join(''); + + const secondaryVisibility = this.props.settings.get('side_arm'); + let showSideArm = secondaryVisibility !== 'none'; let publishText = ''; + let publishText2 = ''; + let title = ''; + let title2 = ''; + + const privacyIcons = { + none: '', + public: 'globe', + unlisted: 'unlock-alt', + private: 'lock', + direct: 'envelope', + }; + + title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`; + + if (showSideArm) { + // Enhanced behavior with dual toot buttons + publishText = ( + <span> + { + <i + className={`fa fa-${privacyIcons[this.props.privacy]}`} + style={{ paddingRight: '5px' }} + /> + }{intl.formatMessage(messages.publish)} + </span> + ); - if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; + title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`; + publishText2 = ( + <i + className={`fa fa-${privacyIcons[secondaryVisibility]}`} + aria-label={title2} + /> + ); } else { - publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + // Original vanilla behavior - no icon if public or unlisted + if (this.props.privacy === 'private' || this.props.privacy === 'direct') { + publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; + } else { + publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + } } + const submitDisabled = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0); + return ( <div className='compose-form'> <Collapsable isVisible={this.props.spoiler} fullHeight={50}> @@ -193,17 +246,35 @@ export default class ComposeForm extends ImmutablePureComponent { <UploadFormContainer /> </div> - <div className='compose-form__buttons-wrapper'> - <div className='compose-form__buttons'> - <UploadButtonContainer /> - <PrivacyDropdownContainer /> - <SensitiveButtonContainer /> - <SpoilerButtonContainer /> - </div> + <div className='compose-form__buttons'> + <ComposeAttachOptions /> + <SensitiveButtonContainer /> + <div className='compose-form__buttons-separator' /> + <PrivacyDropdownContainer /> + <SpoilerButtonContainer /> + <ComposeAdvancedOptionsContainer /> + </div> - <div className='compose-form__publish'> - <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> - <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div> + <div className='compose-form__publish'> + <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> + <div className='compose-form__publish-button-wrapper'> + { + showSideArm ? + <Button + className='compose-form__publish__side-arm' + text={publishText2} + title={title2} + onClick={this.handleSubmit2} + disabled={submitDisabled} + /> : '' + } + <Button + className='compose-form__publish__primary' + text={publishText} + title={title} + onClick={this.handleSubmit} + disabled={submitDisabled} + /> </div> </div> </div> diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 8350d20a5..a3e68643f 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../containers/status_container'; +import StatusContainer from '../../../../glitch/components/status/container'; import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 12d435ded..ffa0a3442 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import ComposeForm from '../components/compose_form'; -import { uploadCompose } from '../../../actions/compose'; +import { changeComposeVisibility, uploadCompose } from '../../../actions/compose'; import { changeCompose, submitCompose, @@ -15,6 +15,7 @@ const mapStateToProps = state => ({ text: state.getIn(['compose', 'text']), suggestion_token: state.getIn(['compose', 'suggestion_token']), suggestions: state.getIn(['compose', 'suggestions']), + advanced_options: state.getIn(['compose', 'advanced_options']), spoiler: state.getIn(['compose', 'spoiler']), spoiler_text: state.getIn(['compose', 'spoiler_text']), privacy: state.getIn(['compose', 'privacy']), @@ -24,6 +25,8 @@ const mapStateToProps = state => ({ is_uploading: state.getIn(['compose', 'is_uploading']), me: state.getIn(['compose', 'me']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), + settings: state.get('local_settings'), + filesAttached: state.getIn(['compose', 'media_attachments']).size > 0, }); const mapDispatchToProps = (dispatch) => ({ @@ -32,6 +35,10 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeCompose(text)); }, + onPrivacyChange (value) { + dispatch(changeComposeVisibility(value)); + }, + onSubmit () { dispatch(submitCompose()); }, diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 0c66585c9..a487f2c89 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; +import { openModal } from '../../actions/modal'; +import { changeLocalSetting } from '../../../glitch/actions/local_settings'; import { Link } from 'react-router-dom'; import { injectIntl, defineMessages } from 'react-intl'; import SearchContainer from './containers/search_container'; @@ -19,7 +21,7 @@ const messages = defineMessages({ notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, }); @@ -48,6 +50,16 @@ export default class Compose extends React.PureComponent { this.props.dispatch(unmountCompose()); } + onLayoutClick = (e) => { + const layout = e.currentTarget.getAttribute('data-mastodon-layout'); + this.props.dispatch(changeLocalSetting(['layout'], layout)); + e.preventDefault(); + } + + openSettings = () => { + this.props.dispatch(openModal('SETTINGS', {})); + } + onFocus = () => { this.props.dispatch(changeComposing(true)); } @@ -78,12 +90,14 @@ export default class Compose extends React.PureComponent { {!columns.some(column => column.get('id') === 'PUBLIC') && ( <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link> )} - <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a> + <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)} aria-label={intl.formatMessage(messages.settings)}><i role='img' className='fa fa-fw fa-cogs' /></a> <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a> </nav> ); } + + return ( <div className='drawer'> {header} @@ -91,7 +105,7 @@ export default class Compose extends React.PureComponent { <SearchContainer /> <div className='drawer__pager'> - <div className='drawer__inner' onFocus={this.onFocus}> + <div className='drawer__inner scrollable optionally-scrollable' onFocus={this.onFocus}> <NavigationContainer onClose={this.onBlur} /> <ComposeFormContainer /> </div> @@ -104,6 +118,7 @@ export default class Compose extends React.PureComponent { } </Motion> </div> + </div> ); } diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 1e1f5873c..8135527c9 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -68,7 +68,7 @@ export default class Favourites extends ImmutablePureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef}> + <Column ref={this.setRef} name='favourites'> <ColumnHeader icon='star' title={intl.formatMessage(messages.heading)} diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 4c9e514cb..94109b151 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -47,14 +47,14 @@ export default class FollowRequests extends ImmutablePureComponent { if (!accountIds) { return ( - <Column> + <Column name='follow-requests'> <LoadingIndicator /> </Column> ); } return ( - <Column icon='users' heading={intl.formatMessage(messages.heading)}> + <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollContainer scrollKey='follow_requests'> diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 973c8a4ae..68267c54f 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -4,6 +4,7 @@ import ColumnLink from '../ui/components/column_link'; import ColumnSubheading from '../ui/components/column_subheading'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { openModal } from '../../actions/modal'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -17,12 +18,14 @@ const messages = defineMessages({ settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, + show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, }); @@ -40,8 +43,18 @@ export default class GettingStarted extends ImmutablePureComponent { me: ImmutablePropTypes.map.isRequired, columns: ImmutablePropTypes.list, multiColumn: PropTypes.bool, + dispatch: PropTypes.func.isRequired, }; + openSettings = () => { + this.props.dispatch(openModal('SETTINGS', {})); + } + + openOnboardingModal = (e) => { + e.preventDefault(); + this.props.dispatch(openModal('ONBOARDING')); + } + render () { const { intl, me, columns, multiColumn } = this.props; @@ -80,28 +93,43 @@ export default class GettingStarted extends ImmutablePureComponent { ]); return ( - <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> - <div className='getting-started__wrapper'> - <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> - {navItems} - <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> - <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> - <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> - </div> + <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> + <div className='scrollable optionally-scrollable'> + <div className='getting-started__wrapper'> + <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> + {navItems} + <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> + <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> + <ColumnLink icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} /> + <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> + <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} /> + <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> + </div> - <div className='getting-started__footer scrollable optionally-scrollable'> - <div className='static-content getting-started'> - <p> - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a> - </p> - <p> - <FormattedMessage - id='getting_started.open_source_notice' - defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' - values={{ github: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> }} - /> - </p> + <div className='getting-started__footer'> + <div className='static-content getting-started'> + <p> + <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'> + <FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /> + </a> • + <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'> + <FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /> + </a> • + <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'> + <FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /> + </a> + </p> + <p> + <FormattedMessage + id='getting_started.open_source_notice' + defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' + values={{ + github: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, + Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a>, + }} + /> + </p> + </div> </div> </div> </Column> diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 5fe21ce90..2077b7cdf 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -91,7 +91,7 @@ export default class HashtagTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef}> + <Column ref={this.setRef} name='hashtag'> <ColumnHeader icon='hashtag' active={hasUnread} diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index a4bc60fac..b35347ba6 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -62,7 +62,7 @@ export default class HomeTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef}> + <Column ref={this.setRef} name='home'> <ColumnHeader icon='home' active={hasUnread} diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 25ca921ae..0f3b8e710 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -54,7 +54,7 @@ export default class Mutes extends ImmutablePureComponent { } return ( - <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollContainer scrollKey='mutes'> <div className='scrollable mutes' onScroll={this.handleScroll}> diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 9d170cad5..903526822 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/notification + import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 921aa460f..fd16c4331 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/notification/container + import { connect } from 'react-redux'; import { makeGetNotification } from '../../../selectors'; import Notification from '../components/notification'; diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 35b430bfb..9c6802482 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,9 +4,13 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; +import { + enterNotificationClearingMode, + expandNotifications, + scrollTopNotifications, +} from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import NotificationContainer from './containers/notification_container'; +import NotificationContainer from '../../../glitch/components/notification/container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { createSelector } from 'reselect'; @@ -25,12 +29,22 @@ const getNotifications = createSelector([ const mapStateToProps = state => ({ notifications: getNotifications(state), + localSettings: state.get('local_settings'), isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: !!state.getIn(['notifications', 'next']), + notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); -@connect(mapStateToProps) +/* glitch */ +const mapDispatchToProps = dispatch => ({ + onEnterCleaningMode(yes) { + dispatch(enterNotificationClearingMode(yes)); + }, + dispatch, +}); + +@connect(mapStateToProps, mapDispatchToProps) @injectIntl export default class Notifications extends React.PureComponent { @@ -44,6 +58,9 @@ export default class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + localSettings: ImmutablePropTypes.map, + notifCleaningActive: PropTypes.bool, + onEnterCleaningMode: PropTypes.func, }; static defaultProps = { @@ -146,7 +163,11 @@ export default class Notifications extends React.PureComponent { ); return ( - <Column ref={this.setColumnRef}> + <Column + ref={this.setColumnRef} + name='notifications' + extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} + > <ColumnHeader icon='bell' active={isUnread} @@ -156,6 +177,10 @@ export default class Notifications extends React.PureComponent { onClick={this.handleHeaderClick} pinned={pinned} multiColumn={multiColumn} + localSettings={this.props.localSettings} + notifCleaning + notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text + onEnterCleaningMode={this.props.onEnterCleaningMode} > <ColumnSettingsContainer /> </ColumnHeader> diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 193489c63..1821bc448 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -79,7 +79,7 @@ export default class PublicTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef}> + <Column ref={this.setRef} name='federated'> <ColumnHeader icon='globe' active={hasUnread} diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 034cc9854..3e94f7446 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -107,8 +107,8 @@ export default class ActionBar extends React.PureComponent { ); let reblogIcon = 'retweet'; - if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; - else if (status.get('visibility') === 'private') reblogIcon = 'lock'; + //if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; + // else if (status.get('visibility') === 'private') reblogIcon = 'lock'; let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index c10e2c531..d8547db36 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -3,14 +3,16 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; -import StatusContent from '../../../components/status_content'; -import MediaGallery from '../../../components/media_gallery'; +import StatusContent from '../../../../glitch/components/status/content'; +import StatusGallery from '../../../../glitch/components/status/gallery'; +import StatusPlayer from '../../../../glitch/components/status/player'; import AttachmentList from '../../../components/attachment_list'; import { Link } from 'react-router-dom'; import { FormattedDate, FormattedNumber } from 'react-intl'; import CardContainer from '../containers/card_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Video from '../../video'; +// import Video from '../../video'; +import VisibilityIcon from '../../../../glitch/components/status/visibility_icon'; export default class DetailedStatus extends ImmutablePureComponent { @@ -20,6 +22,7 @@ export default class DetailedStatus extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + settings: ImmutablePropTypes.map.isRequired, onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, autoPlayGif: PropTypes.bool, @@ -34,14 +37,16 @@ export default class DetailedStatus extends ImmutablePureComponent { e.stopPropagation(); } - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); - } + // handleOpenVideo = startTime => { + // this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + // } render () { const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + const { settings } = this.props; let media = ''; + let mediaIcon = null; let applicationLink = ''; let reblogLink = ''; let reblogIcon = 'retweet'; @@ -50,33 +55,33 @@ export default class DetailedStatus extends ImmutablePureComponent { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = <AttachmentList media={status.get('media_attachments')} />; } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); - media = ( - <Video - preview={video.get('preview_url')} - src={video.get('url')} - width={300} - height={150} - onOpenVideo={this.handleOpenVideo} + <StatusPlayer sensitive={status.get('sensitive')} + media={status.getIn(['media_attachments', 0])} + letterbox={settings.getIn(['media', 'letterbox'])} + fullwidth={settings.getIn(['media', 'fullwidth'])} + height={250} + onOpenVideo={this.props.onOpenVideo} + autoplay /> ); + mediaIcon = 'video-camera'; } else { media = ( - <MediaGallery - standalone + <StatusGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} - height={300} + letterbox={settings.getIn(['media', 'letterbox'])} + fullwidth={settings.getIn(['media', 'fullwidth'])} + height={250} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} /> ); + mediaIcon = 'picture-o'; } - } else if (status.get('spoiler_text').length === 0) { - media = <CardContainer statusId={status.get('id')} />; - } + } else media = <CardContainer statusId={status.get('id')} />; if (status.get('application')) { applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; @@ -106,9 +111,11 @@ export default class DetailedStatus extends ImmutablePureComponent { <DisplayName account={status.get('account')} /> </a> - <StatusContent status={status} /> - - {media} + <StatusContent + status={status} + media={media} + mediaIcon={mediaIcon} + /> <div className='detailed-status__meta'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> @@ -118,7 +125,7 @@ export default class DetailedStatus extends ImmutablePureComponent { <span className='detailed-status__favorites'> <FormattedNumber value={status.get('favourites_count')} /> </span> - </Link> + </Link> · <VisibilityIcon visibility={status.get('visibility')} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 7ad3a7644..c40630a0a 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -24,7 +24,7 @@ import { initReport } from '../../actions/reports'; import { makeGetStatus } from '../../selectors'; import { ScrollContainer } from 'react-router-scroll'; import ColumnBackButton from '../../components/column_back_button'; -import StatusContainer from '../../containers/status_container'; +import StatusContainer from '../../../glitch/components/status/container'; import { openModal } from '../../actions/modal'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -40,6 +40,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props.params.statusId), + settings: state.get('local_settings'), ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), me: state.getIn(['meta', 'me']), @@ -63,6 +64,7 @@ export default class Status extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, + settings: ImmutablePropTypes.map.isRequired, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, me: PropTypes.string, @@ -250,14 +252,16 @@ export default class Status extends ImmutablePureComponent { if (status && ancestorsIds && ancestorsIds.size > 0) { const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; - element.scrollIntoView(true); - this._scrolledIntoView = true; + if (element) { + element.scrollIntoView(true); + this._scrolledIntoView = true; + } } } render () { let ancestors, descendants; - const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props; + const { status, settings, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props; if (status === null) { return ( @@ -298,6 +302,7 @@ export default class Status extends ImmutablePureComponent { <div className='focusable' tabIndex='0'> <DetailedStatus status={status} + settings={settings} autoPlayGif={autoPlayGif} me={me} onOpenVideo={this.handleOpenVideo} diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 0e9592c97..dfd1284e9 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Button from '../../../components/button'; -import StatusContent from '../../../components/status_content'; +import StatusContent from '../../../../glitch/components/status/content'; import Avatar from '../../../components/avatar'; import RelativeTimestamp from '../../../components/relative_timestamp'; import DisplayName from '../../../components/display_name'; diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js index 15538ea38..c1700f86e 100644 --- a/app/javascript/mastodon/features/ui/components/column.js +++ b/app/javascript/mastodon/features/ui/components/column.js @@ -13,6 +13,7 @@ export default class Column extends React.PureComponent { children: PropTypes.node, active: PropTypes.bool, hideHeadingOnMobile: PropTypes.bool, + name: PropTypes.string, }; handleHeaderClick = () => { @@ -47,7 +48,7 @@ export default class Column extends React.PureComponent { } render () { - const { heading, icon, children, active, hideHeadingOnMobile } = this.props; + const { heading, icon, children, active, hideHeadingOnMobile, name } = this.props; const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth))); @@ -59,6 +60,7 @@ export default class Column extends React.PureComponent { <div ref={this.setRef} role='region' + data-column={name} aria-labelledby={columnHeaderId} className='column' onScroll={this.handleScroll} diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js index 5425219c4..b845d1895 100644 --- a/app/javascript/mastodon/features/ui/components/column_link.js +++ b/app/javascript/mastodon/features/ui/components/column_link.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -const ColumnLink = ({ icon, text, to, href, method }) => { +const ColumnLink = ({ icon, text, to, onClick, href, method }) => { if (href) { return ( <a href={href} className='column-link' data-method={method}> @@ -10,13 +10,20 @@ const ColumnLink = ({ icon, text, to, href, method }) => { {text} </a> ); - } else { + } else if (to) { return ( <Link to={to} className='column-link'> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} </Link> ); + } else { + return ( + <a onClick={onClick} className='column-link' role='button' tabIndex='0' data-method={method}> + <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + {text} + </a> + ); } }; @@ -24,9 +31,9 @@ ColumnLink.propTypes = { icon: PropTypes.string.isRequired, text: PropTypes.string.isRequired, to: PropTypes.string, + onClick: PropTypes.func, href: PropTypes.string, method: PropTypes.string, - hideOnMobile: PropTypes.bool, }; export default ColumnLink; diff --git a/app/javascript/mastodon/features/ui/components/doodle_modal.js b/app/javascript/mastodon/features/ui/components/doodle_modal.js new file mode 100644 index 000000000..4efc9d2e6 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/doodle_modal.js @@ -0,0 +1,614 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from '../../../components/button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Atrament from 'atrament'; // the doodling library +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { doodleSet, uploadCompose } from '../../../actions/compose'; +import IconButton from '../../../components/icon_button'; +import { debounce, mapValues } from 'lodash'; +import classNames from 'classnames'; + +// palette nicked from MyPaint, CC0 +const palette = [ + ['rgb( 0, 0, 0)', 'Black'], + ['rgb( 38, 38, 38)', 'Gray 15'], + ['rgb( 77, 77, 77)', 'Grey 30'], + ['rgb(128, 128, 128)', 'Grey 50'], + ['rgb(171, 171, 171)', 'Grey 67'], + ['rgb(217, 217, 217)', 'Grey 85'], + ['rgb(255, 255, 255)', 'White'], + ['rgb(128, 0, 0)', 'Maroon'], + ['rgb(209, 0, 0)', 'English-red'], + ['rgb(255, 54, 34)', 'Tomato'], + ['rgb(252, 60, 3)', 'Orange-red'], + ['rgb(255, 140, 105)', 'Salmon'], + ['rgb(252, 232, 32)', 'Cadium-yellow'], + ['rgb(243, 253, 37)', 'Lemon yellow'], + ['rgb(121, 5, 35)', 'Dark crimson'], + ['rgb(169, 32, 62)', 'Deep carmine'], + ['rgb(255, 140, 0)', 'Orange'], + ['rgb(255, 168, 18)', 'Dark tangerine'], + ['rgb(217, 144, 88)', 'Persian orange'], + ['rgb(194, 178, 128)', 'Sand'], + ['rgb(255, 229, 180)', 'Peach'], + ['rgb(100, 54, 46)', 'Bole'], + ['rgb(108, 41, 52)', 'Dark cordovan'], + ['rgb(163, 65, 44)', 'Chestnut'], + ['rgb(228, 136, 100)', 'Dark salmon'], + ['rgb(255, 195, 143)', 'Apricot'], + ['rgb(255, 219, 188)', 'Unbleached silk'], + ['rgb(242, 227, 198)', 'Straw'], + ['rgb( 53, 19, 13)', 'Bistre'], + ['rgb( 84, 42, 14)', 'Dark chocolate'], + ['rgb(102, 51, 43)', 'Burnt sienna'], + ['rgb(184, 66, 0)', 'Sienna'], + ['rgb(216, 153, 12)', 'Yellow ochre'], + ['rgb(210, 180, 140)', 'Tan'], + ['rgb(232, 204, 144)', 'Dark wheat'], + ['rgb( 0, 49, 83)', 'Prussian blue'], + ['rgb( 48, 69, 119)', 'Dark grey blue'], + ['rgb( 0, 71, 171)', 'Cobalt blue'], + ['rgb( 31, 117, 254)', 'Blue'], + ['rgb(120, 180, 255)', 'Bright french blue'], + ['rgb(171, 200, 255)', 'Bright steel blue'], + ['rgb(208, 231, 255)', 'Ice blue'], + ['rgb( 30, 51, 58)', 'Medium jungle green'], + ['rgb( 47, 79, 79)', 'Dark slate grey'], + ['rgb( 74, 104, 93)', 'Dark grullo green'], + ['rgb( 0, 128, 128)', 'Teal'], + ['rgb( 67, 170, 176)', 'Turquoise'], + ['rgb(109, 174, 199)', 'Cerulean frost'], + ['rgb(173, 217, 186)', 'Tiffany green'], + ['rgb( 22, 34, 29)', 'Gray-asparagus'], + ['rgb( 36, 48, 45)', 'Medium dark teal'], + ['rgb( 74, 104, 93)', 'Xanadu'], + ['rgb(119, 198, 121)', 'Mint'], + ['rgb(175, 205, 182)', 'Timberwolf'], + ['rgb(185, 245, 246)', 'Celeste'], + ['rgb(193, 255, 234)', 'Aquamarine'], + ['rgb( 29, 52, 35)', 'Cal Poly Pomona'], + ['rgb( 1, 68, 33)', 'Forest green'], + ['rgb( 42, 128, 0)', 'Napier green'], + ['rgb(128, 128, 0)', 'Olive'], + ['rgb( 65, 156, 105)', 'Sea green'], + ['rgb(189, 246, 29)', 'Green-yellow'], + ['rgb(231, 244, 134)', 'Bright chartreuse'], + ['rgb(138, 23, 137)', 'Purple'], + ['rgb( 78, 39, 138)', 'Violet'], + ['rgb(193, 75, 110)', 'Dark thulian pink'], + ['rgb(222, 49, 99)', 'Cerise'], + ['rgb(255, 20, 147)', 'Deep pink'], + ['rgb(255, 102, 204)', 'Rose pink'], + ['rgb(255, 203, 219)', 'Pink'], + ['rgb(255, 255, 255)', 'White'], + ['rgb(229, 17, 1)', 'RGB Red'], + ['rgb( 0, 255, 0)', 'RGB Green'], + ['rgb( 0, 0, 255)', 'RGB Blue'], + ['rgb( 0, 255, 255)', 'CMYK Cyan'], + ['rgb(255, 0, 255)', 'CMYK Magenta'], + ['rgb(255, 255, 0)', 'CMYK Yellow'], +]; + +// re-arrange to the right order for display +let palReordered = []; +for (let row = 0; row < 7; row++) { + for (let col = 0; col < 11; col++) { + palReordered.push(palette[col * 7 + row]); + } + palReordered.push(null); // null indicates a <br /> +} + +// Utility for converting base64 image to binary for upload +// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f +function dataURLtoFile(dataurl, filename) { + let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while(n--){ + u8arr[n] = bstr.charCodeAt(n); + } + return new File([u8arr], filename, { type: mime }); +} + +const DOODLE_SIZES = { + normal: [500, 500, 'Square 500'], + tootbanner: [702, 330, 'Tootbanner'], + s640x480: [640, 480, '640×480 - 480p'], + s800x600: [800, 600, '800×600 - SVGA'], + s720x480: [720, 405, '720x405 - 16:9'], +}; + + +const mapStateToProps = state => ({ + options: state.getIn(['compose', 'doodle']), +}); + +const mapDispatchToProps = dispatch => ({ + /** Set options in the redux store */ + setOpt: (opts) => dispatch(doodleSet(opts)), + /** Submit doodle for upload */ + submit: (file) => dispatch(uploadCompose([file])), +}); + +/** + * Doodling dialog with drawing canvas + * + * Keyboard shortcuts: + * - Delete: Clear screen, fill with background color + * - Backspace, Ctrl+Z: Undo one step + * - Ctrl held while drawing: Use background color + * - Shift held while clicking screen: Use fill tool + * + * Palette: + * - Left mouse button: pick foreground + * - Ctrl + left mouse button: pick background + * - Right mouse button: pick background + */ +@connect(mapStateToProps, mapDispatchToProps) +export default class DoodleModal extends ImmutablePureComponent { + + static propTypes = { + options: ImmutablePropTypes.map, + onClose: PropTypes.func.isRequired, + setOpt: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, + }; + + //region Option getters/setters + + /** Foreground color */ + get fg () { + return this.props.options.get('fg'); + } + set fg (value) { + this.props.setOpt({ fg: value }); + } + + /** Background color */ + get bg () { + return this.props.options.get('bg'); + } + set bg (value) { + this.props.setOpt({ bg: value }); + } + + /** Swap Fg and Bg for drawing */ + get swapped () { + return this.props.options.get('swapped'); + } + set swapped (value) { + this.props.setOpt({ swapped: value }); + } + + /** Mode - 'draw' or 'fill' */ + get mode () { + return this.props.options.get('mode'); + } + set mode (value) { + this.props.setOpt({ mode: value }); + } + + /** Base line weight */ + get weight () { + return this.props.options.get('weight'); + } + set weight (value) { + this.props.setOpt({ weight: value }); + } + + /** Drawing opacity */ + get opacity () { + return this.props.options.get('opacity'); + } + set opacity (value) { + this.props.setOpt({ opacity: value }); + } + + /** Adaptive stroke - change width with speed */ + get adaptiveStroke () { + return this.props.options.get('adaptiveStroke'); + } + set adaptiveStroke (value) { + this.props.setOpt({ adaptiveStroke: value }); + } + + /** Smoothing (for mouse drawing) */ + get smoothing () { + return this.props.options.get('smoothing'); + } + set smoothing (value) { + this.props.setOpt({ smoothing: value }); + } + + /** Size preset */ + get size () { + return this.props.options.get('size'); + } + set size (value) { + this.props.setOpt({ size: value }); + } + + //endregion + + /** Key up handler */ + handleKeyUp = (e) => { + if (e.target.nodeName === 'INPUT') return; + + if (e.key === 'Delete') { + e.preventDefault(); + this.handleClearBtn(); + return; + } + + if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) { + e.preventDefault(); + this.undo(); + } + + if (e.key === 'Control' || e.key === 'Meta') { + this.controlHeld = false; + this.swapped = false; + } + + if (e.key === 'Shift') { + this.shiftHeld = false; + this.mode = 'draw'; + } + }; + + /** Key down handler */ + handleKeyDown = (e) => { + if (e.key === 'Control' || e.key === 'Meta') { + this.controlHeld = true; + this.swapped = true; + } + + if (e.key === 'Shift') { + this.shiftHeld = true; + this.mode = 'fill'; + } + }; + + /** + * Component installed in the DOM, do some initial set-up + */ + componentDidMount () { + this.controlHeld = false; + this.shiftHeld = false; + this.swapped = false; + window.addEventListener('keyup', this.handleKeyUp, false); + window.addEventListener('keydown', this.handleKeyDown, false); + }; + + /** + * Tear component down + */ + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp, false); + window.removeEventListener('keydown', this.handleKeyDown, false); + if (this.sketcher) this.sketcher.destroy(); + } + + /** + * Set reference to the canvas element. + * This is called during component init + * + * @param elem - canvas element + */ + setCanvasRef = (elem) => { + this.canvas = elem; + if (elem) { + elem.addEventListener('dirty', () => { + this.saveUndo(); + this.sketcher._dirty = false; + }); + + elem.addEventListener('click', () => { + // sketcher bug - does not fire dirty on fill + if (this.mode === 'fill') { + this.saveUndo(); + } + }); + + // prevent context menu + elem.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); + + elem.addEventListener('mousedown', (e) => { + if (e.button === 2) { + this.swapped = true; + } + }); + + elem.addEventListener('mouseup', (e) => { + if (e.button === 2) { + this.swapped = this.controlHeld; + } + }); + + this.initSketcher(elem); + this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill' + } + }; + + /** + * Set up the sketcher instance + * + * @param canvas - canvas element. Null if we're just resizing + */ + initSketcher (canvas = null) { + const sizepreset = DOODLE_SIZES[this.size]; + + if (this.sketcher) this.sketcher.destroy(); + this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]); + + if (canvas) { + this.ctx = this.sketcher.context; + this.updateSketcherSettings(); + } + + this.clearScreen(); + } + + /** + * Done button handler + */ + onDoneButton = () => { + const dataUrl = this.sketcher.toImage(); + const file = dataURLtoFile(dataUrl, 'doodle.png'); + this.props.submit(file); + this.props.onClose(); // close dialog + }; + + /** + * Cancel button handler + */ + onCancelButton = () => { + if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) { + return; + } + + this.props.onClose(); // close dialog + }; + + /** + * Update sketcher options based on state + */ + updateSketcherSettings () { + if (!this.sketcher) return; + + if (this.oldSize !== this.size) this.initSketcher(); + + this.sketcher.color = (this.swapped ? this.bg : this.fg); + this.sketcher.opacity = this.opacity; + this.sketcher.weight = this.weight; + this.sketcher.mode = this.mode; + this.sketcher.smoothing = this.smoothing; + this.sketcher.adaptiveStroke = this.adaptiveStroke; + + this.oldSize = this.size; + } + + /** + * Fill screen with background color + */ + clearScreen = () => { + this.ctx.fillStyle = this.bg; + this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2); + this.undos = []; + + this.doSaveUndo(); + }; + + /** + * Undo one step + */ + undo = () => { + if (this.undos.length > 1) { + this.undos.pop(); + const buf = this.undos.pop(); + + this.sketcher.clear(); + this.ctx.putImageData(buf, 0, 0); + this.doSaveUndo(); + } + }; + + /** + * Save canvas content into the undo buffer immediately + */ + doSaveUndo = () => { + this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)); + }; + + /** + * Called on each canvas change. + * Saves canvas content to the undo buffer after some period of inactivity. + */ + saveUndo = debounce(() => { + this.doSaveUndo(); + }, 100); + + /** + * Palette left click. + * Selects Fg color (or Bg, if Control/Meta is held) + * + * @param e - event + */ + onPaletteClick = (e) => { + const c = e.target.dataset.color; + + if (this.controlHeld) { + this.bg = c; + } else { + this.fg = c; + } + + e.target.blur(); + e.preventDefault(); + }; + + /** + * Palette right click. + * Selects Bg color + * + * @param e - event + */ + onPaletteRClick = (e) => { + this.bg = e.target.dataset.color; + e.target.blur(); + e.preventDefault(); + }; + + /** + * Handle click on the Draw mode button + * + * @param e - event + */ + setModeDraw = (e) => { + this.mode = 'draw'; + e.target.blur(); + }; + + /** + * Handle click on the Fill mode button + * + * @param e - event + */ + setModeFill = (e) => { + this.mode = 'fill'; + e.target.blur(); + }; + + /** + * Handle click on Smooth checkbox + * + * @param e - event + */ + tglSmooth = (e) => { + this.smoothing = !this.smoothing; + e.target.blur(); + }; + + /** + * Handle click on Adaptive checkbox + * + * @param e - event + */ + tglAdaptive = (e) => { + this.adaptiveStroke = !this.adaptiveStroke; + e.target.blur(); + }; + + /** + * Handle change of the Weight input field + * + * @param e - event + */ + setWeight = (e) => { + this.weight = +e.target.value || 1; + }; + + /** + * Set size - clalback from the select box + * + * @param e - event + */ + changeSize = (e) => { + let newSize = e.target.value; + if (newSize === this.oldSize) return; + + if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) { + return; + } + + this.size = newSize; + }; + + handleClearBtn = () => { + if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) { + return; + } + + this.clearScreen(); + }; + + /** + * Render the component + */ + render () { + this.updateSketcherSettings(); + + return ( + <div className='modal-root__modal doodle-modal'> + <div className='doodle-modal__container'> + <canvas ref={this.setCanvasRef} /> + </div> + + <div className='doodle-modal__action-bar'> + <div className='doodle-toolbar'> + <Button text='Done' onClick={this.onDoneButton} /> + <Button text='Cancel' onClick={this.onCancelButton} /> + </div> + <div className='filler' /> + <div className='doodle-toolbar with-inputs'> + <div> + <label htmlFor='dd_smoothing'>Smoothing</label> + <span className='val'> + <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} /> + </span> + </div> + <div> + <label htmlFor='dd_adaptive'>Adaptive</label> + <span className='val'> + <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} /> + </span> + </div> + <div> + <label htmlFor='dd_weight'>Weight</label> + <span className='val'> + <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} /> + </span> + </div> + <div> + <select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}> + { Object.values(mapValues(DOODLE_SIZES, (val, k) => + <option key={k} value={k}>{val[2]}</option> + )) } + </select> + </div> + </div> + <div className='doodle-toolbar'> + <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted /> + <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted /> + <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted /> + <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted /> + </div> + <div className='doodle-palette'> + { + palReordered.map((c, i) => + c === null ? + <br key={i} /> : + <button + key={i} + style={{ backgroundColor: c[0] }} + onClick={this.onPaletteClick} + onContextMenu={this.onPaletteRClick} + data-color={c[0]} + title={c[1]} + className={classNames({ + 'foreground': this.fg === c[0], + 'background': this.bg === c[0], + })} + /> + ) + } + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index f420f0abf..3e56fbf8e 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -7,10 +7,13 @@ import ActionsModal from './actions_modal'; import MediaModal from './media_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; +import DoodleModal from './doodle_modal'; import ConfirmationModal from './confirmation_modal'; import { OnboardingModal, + MuteModal, ReportModal, + SettingsModal, EmbedModal, } from '../../../features/ui/util/async-components'; @@ -19,8 +22,11 @@ const MODAL_COMPONENTS = { 'ONBOARDING': OnboardingModal, 'VIDEO': () => Promise.resolve({ default: VideoModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), + 'DOODLE': () => Promise.resolve({ default: DoodleModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), + 'MUTE': MuteModal, 'REPORT': ReportModal, + 'SETTINGS': SettingsModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, }; @@ -39,7 +45,7 @@ export default class ModalRoot extends React.PureComponent { handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.type) { + && !!this.props.type && !this.props.props.noEsc) { this.props.onClose(); } } @@ -84,7 +90,7 @@ export default class ModalRoot extends React.PureComponent { } renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; + return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; } renderError = (props) => { diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js new file mode 100644 index 000000000..b5e83bb71 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Button from '../../../components/button'; +import { closeModal } from '../../../actions/modal'; +import { muteAccount } from '../../../actions/accounts'; +import { toggleHideNotifications } from '../../../actions/mutes'; + + +const mapStateToProps = state => { + return { + isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), + account: state.getIn(['mutes', 'new', 'account']), + notifications: state.getIn(['mutes', 'new', 'notifications']), + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onConfirm(account, notifications) { + dispatch(muteAccount(account.get('id'), notifications)); + }, + + onClose() { + dispatch(closeModal()); + }, + + onToggleNotifications() { + dispatch(toggleHideNotifications()); + }, + }; +}; + +@connect(mapStateToProps, mapDispatchToProps) +@injectIntl +export default class MuteModal extends React.PureComponent { + + static propTypes = { + isSubmitting: PropTypes.bool.isRequired, + account: PropTypes.object.isRequired, + notifications: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + onToggleNotifications: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.button.focus(); + } + + handleClick = () => { + this.props.onClose(); + this.props.onConfirm(this.props.account, this.props.notifications); + } + + handleCancel = () => { + this.props.onClose(); + } + + setRef = (c) => { + this.button = c; + } + + toggleNotifications = () => { + this.props.onToggleNotifications(); + } + + render () { + const { account, notifications } = this.props; + + return ( + <div className='modal-root__modal mute-modal'> + <div className='mute-modal__container'> + <p> + <FormattedMessage + id='confirmations.mute.message' + defaultMessage='Are you sure you want to mute {name}?' + values={{ name: <strong>@{account.get('acct')}</strong> }} + /> + </p> + <p> + <label htmlFor='mute-modal__hide-notifications-checkbox'> + <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> + <input id='mute-modal__hide-notifications-checkbox' type='checkbox' checked={notifications} onChange={this.toggleNotifications} /> + </label> + </p> + </div> + + <div className='mute-modal__action-bar'> + <Button onClick={this.handleCancel} className='mute-modal__cancel-button'> + <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> + </Button> + <Button onClick={this.handleClick} ref={this.setRef}> + <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' /> + </Button> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index 7905bca2e..daf6b485c 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -10,7 +10,10 @@ import ComposeForm from '../../compose/components/compose_form'; import Search from '../../compose/components/search'; import NavigationBar from '../../compose/components/navigation_bar'; import ColumnHeader from './column_header'; -import { List as ImmutableList } from 'immutable'; +import { + List as ImmutableList, + Map as ImmutableMap, +} from 'immutable'; const noop = () => { }; @@ -28,8 +31,8 @@ const PageOne = ({ acct, domain }) => ( </div> <div> - <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1> - <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' /></p> + <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1> + <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p> <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p> </div> </div> @@ -44,7 +47,7 @@ const PageTwo = ({ me }) => ( <div className='onboarding-modal__page onboarding-modal__page-two'> <div className='figure non-interactive'> <div className='pseudo-drawer'> - <NavigationBar account={me} /> + <NavigationBar onClose={noop} account={me} /> </div> <ComposeForm text='Awoo! #introductions' @@ -59,7 +62,9 @@ const PageTwo = ({ me }) => ( onClearSuggestions={noop} onFetchSuggestions={noop} onSuggestionSelected={noop} + onPrivacyChange={noop} showSearch + settings={ImmutableMap.of('side_arm', 'none')} /> </div> @@ -83,7 +88,7 @@ const PageThree = ({ me }) => ( /> <div className='pseudo-drawer'> - <NavigationBar account={me} /> + <NavigationBar onClose={noop} account={me} /> </div> </div> @@ -148,8 +153,8 @@ const PageSix = ({ admin, domain }) => { <div className='onboarding-modal__page onboarding-modal__page-six'> <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1> {adminSection} - <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> - <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> + <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ domain, fork: <a href='https://en.wikipedia.org/wiki/Fork_(software_development)' target='_blank' rel='noopener'>fork</a>, Mastodon: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>Mastodon</a>, github: <a href='https://github.com/glitch-soc/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> + <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ domain, apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p> </div> ); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 70e451373..883bfe055 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -15,6 +15,7 @@ import { clearHeight } from '../../actions/height_cache'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; +import classNames from 'classnames'; import { Compose, Status, @@ -41,9 +42,13 @@ import { HotKeys } from 'react-hotkeys'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. -import '../../components/status'; +import '../../../glitch/components/status'; const mapStateToProps = state => ({ + systemFontUi: state.getIn(['meta', 'system_font_ui']), + layout: state.getIn(['local_settings', 'layout']), + isWide: state.getIn(['local_settings', 'stretch']), + navbarUnder: state.getIn(['local_settings', 'navbar_under']), me: state.getIn(['meta', 'me']), isComposing: state.getIn(['compose', 'is_composing']), }); @@ -85,6 +90,10 @@ export default class UI extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, + layout: PropTypes.string, + isWide: PropTypes.bool, + systemFontUi: PropTypes.bool, + navbarUnder: PropTypes.bool, isComposing: PropTypes.bool, me: PropTypes.string, location: PropTypes.object, @@ -194,6 +203,7 @@ export default class UI extends React.Component { if (nextProps.isComposing !== this.props.isComposing) { // Avoid expensive update just to toggle a class this.node.classList.toggle('is-composing', nextProps.isComposing); + this.node.classList.toggle('navbar-under', nextProps.navbarUnder); return false; } @@ -318,7 +328,24 @@ export default class UI extends React.Component { render () { const { width, draggingOver } = this.state; - const { children } = this.props; + const { children, layout, isWide, navbarUnder } = this.props; + + const columnsClass = layout => { + switch (layout) { + case 'single': + return 'single-column'; + case 'multiple': + return 'multi-columns'; + default: + return 'auto-columns'; + } + }; + + const className = classNames('ui', columnsClass(layout), { + 'wide': isWide, + 'system-font': this.props.systemFontUi, + 'navbar-under': navbarUnder, + }); const handlers = { new: this.handleHotkeyNew, @@ -340,10 +367,10 @@ export default class UI extends React.Component { return ( <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> - <div className='ui' ref={this.setRef}> - <TabsBar /> + <div className={className} ref={this.setRef}> + {navbarUnder ? null : (<TabsBar />)} - <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}> + <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}> <WrappedSwitch> <Redirect from='/' to='/getting-started' exact /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> @@ -375,6 +402,7 @@ export default class UI extends React.Component { </ColumnsAreaContainer> <NotificationsContainer /> + {navbarUnder ? (<TabsBar />) : null} <LoadingBarContainer className='loading-bar' /> <ModalContainer /> <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 8f7b91d21..7f2b303a7 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -86,10 +86,21 @@ export function OnboardingModal () { return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); } +export function MuteModal () { + return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal'); +} + export function ReportModal () { return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); } +export function SettingsModal () { + return import(/* webpackChunkName: "modals/settings_modal" */'glitch/components/local_settings/container'); +} + +// THESE AREN'T USED BY US; SEE `glitch/components/status` AND `mastodon/features/status`. // +// IF MASTODON EVER CHANGES DETAILED STATUSES TO REQUIRE THEM, WE'LL NEED TO UPDATE THE URLS OR SOMETHING LOL. // + export function MediaGallery () { return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery'); } |