diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
18 files changed, 723 insertions, 455 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index 25c2622d8..506c668a7 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -2,9 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; // Actions. import { @@ -43,7 +40,7 @@ import { countableText } from 'flavours/glitch/util/counter'; import { me } from 'flavours/glitch/util/initial_state'; import { isMobile } from 'flavours/glitch/util/is_mobile'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -import { mergeProps } from 'flavours/glitch/util/redux_helpers'; +import { wrap } from 'flavours/glitch/util/redux_helpers'; // State mapping. function mapStateToProps (state) { @@ -204,9 +201,7 @@ const handlers = { }; // The component. -@injectIntl -@connect(mapStateToProps, mapDispatchToProps, mergeProps) -export default class Composer extends React.Component { +class Composer extends React.Component { // Constructor. constructor (props) { @@ -408,7 +403,7 @@ export default class Composer extends React.Component { // Context Composer.contextTypes = { history: PropTypes.object, -} +}; // Props. Composer.propTypes = { @@ -438,3 +433,7 @@ Composer.propTypes = { text: PropTypes.string, }).isRequired, }; + +// Connecting and export. +export { Composer as WrappedComponent }; +export default wrap(Composer, mapStateToProps, mapDispatchToProps, true); diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js index 0f304bc88..ee52008a7 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -70,7 +70,7 @@ const handlers = { // dropdown. if (onModalClose && isUserTouching()) { if (open) { - onModalClose() + onModalClose(); } else if (onChange && onModalOpen) { onModalOpen({ actions: items.map( diff --git a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js b/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js deleted file mode 100644 index 1b6d74123..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'flavours/glitch/components/avatar'; -import IconButton from 'flavours/glitch/components/icon_button'; -import Permalink from 'flavours/glitch/components/permalink'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class NavigationBar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render () { - return ( - <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> - <Avatar account={this.props.account} size={40} /> - </Permalink> - - <div className='navigation-bar__profile'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> - </Permalink> - - <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - </div> - - <IconButton title='' icon='close' onClick={this.props.onClose} /> - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/components/search.js b/app/javascript/flavours/glitch/features/drawer/components/search.js deleted file mode 100644 index 1ce66b19d..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/search.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, -}); - -class SearchPopout extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - }; - - render () { - const { style } = this.props; - - return ( - <div style={{ ...style, position: 'absolute', width: 285 }}> - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' /> - </div> - )} - </Motion> - </div> - ); - } - -} - -@injectIntl -export default class Search extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - expanded: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleClear = (e) => { - e.preventDefault(); - - if (this.props.value.length > 0 || this.props.submitted) { - this.props.onClear(); - } - } - - handleKeyDown = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onSubmit(); - } else if (e.key === 'Escape') { - document.querySelector('.ui').parentElement.focus(); - } - } - - noop () { - - } - - handleFocus = () => { - this.setState({ expanded: true }); - this.props.onShow(); - } - - handleBlur = () => { - this.setState({ expanded: false }); - } - - render () { - const { intl, value, submitted } = this.props; - const { expanded } = this.state; - const hasValue = value.length > 0 || submitted; - - return ( - <div className='search'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> - <input - className='search__input' - type='text' - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyDown} - onFocus={this.handleFocus} - onBlur={this.handleBlur} - /> - </label> - - <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> - <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> - </div> - - <Overlay show={expanded && !hasValue} placement='bottom' target={this}> - <SearchPopout /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/components/search_results.js b/app/javascript/flavours/glitch/features/drawer/components/search_results.js deleted file mode 100644 index 2a4818d4e..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/search_results.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import StatusContainer from 'flavours/glitch/containers/status_container'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class SearchResults extends ImmutablePureComponent { - - static propTypes = { - results: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { results } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( - <div className='search-results__section'> - {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} - </div> - ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( - <div className='search-results__section'> - {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} - </div> - ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( - <div className='search-results__section'> - {results.get('hashtags').map(hashtag => - <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> - #{hashtag} - </Link> - )} - </div> - ); - } - - return ( - <div className='search-results'> - <div className='search-results__header'> - <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> - </div> - - {accounts} - {statuses} - {hashtags} - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js b/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js deleted file mode 100644 index eb630ffbb..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux'; -import NavigationBar from '../components/navigation_bar'; -import { me } from 'flavours/glitch/util/initial_state'; - -const mapStateToProps = state => { - return { - account: state.getIn(['accounts', me]), - }; -}; - -export default connect(mapStateToProps)(NavigationBar); diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_container.js deleted file mode 100644 index 8f4bfcf08..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { - changeSearch, - clearSearch, - submitSearch, - showSearch, -} from 'flavours/glitch/actions/search'; -import Search from '../components/search'; - -const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']), - submitted: state.getIn(['search', 'submitted']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeSearch(value)); - }, - - onClear () { - dispatch(clearSearch()); - }, - - onSubmit () { - dispatch(submitSearch()); - }, - - onShow () { - dispatch(showSearch()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js deleted file mode 100644 index 16d95d417..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import SearchResults from '../components/search_results'; - -const mapStateToProps = state => ({ - results: state.getIn(['search', 'results']), -}); - -export default connect(mapStateToProps)(SearchResults); diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js new file mode 100644 index 000000000..fd79b6e18 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/header/index.js @@ -0,0 +1,117 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages } from 'react-intl'; +import { Link } from 'react-router-dom'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { conditionalRender } from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + community: { + defaultMessage: 'Local timeline', + id: 'navigation_bar.community_timeline', + }, + home_timeline: { + defaultMessage: 'Home', + id: 'tabs_bar.home', + }, + logout: { + defaultMessage: 'Logout', + id: 'navigation_bar.logout', + }, + notifications: { + defaultMessage: 'Notifications', + id: 'tabs_bar.notifications', + }, + public: { + defaultMessage: 'Federated timeline', + id: 'navigation_bar.public_timeline', + }, + settings: { + defaultMessage: 'App settings', + id: 'navigation_bar.app_settings', + }, + start: { + defaultMessage: 'Getting started', + id: 'getting_started.heading', + }, +}); + +// The component. +export default function DrawerHeader ({ + columns, + intl, + onSettingsClick, +}) { + + // Only renders the component if the column isn't being shown. + const renderForColumn = conditionalRender.bind( + columnId => !columns || !columns.some( + column => column.get('id') === columnId + ) + ); + + // The result. + return ( + <nav className='drawer--header'> + <Link + aria-label={intl.formatMessage(messages.start)} + title={intl.formatMessage(messages.start)} + to='/getting-started' + ><Icon icon='asterisk' /></Link> + {renderForColumn('HOME', ( + <Link + aria-label={intl.formatMessage(messages.home_timeline)} + title={intl.formatMessage(messages.home_timeline)} + to='/timelines/home' + ><Icon icon='home' /></Link> + ))} + {renderForColumn('NOTIFICATIONS', ( + <Link + aria-label={intl.formatMessage(messages.notifications)} + title={intl.formatMessage(messages.notifications)} + to='/notifications' + ><Icon icon='bell' /></Link> + ))} + {renderForColumn('COMMUNITY', ( + <Link + aria-label={intl.formatMessage(messages.community)} + title={intl.formatMessage(messages.community)} + to='/timelines/public/local' + ><Icon icon='users' /></Link> + ))} + {renderForColumn('PUBLIC', ( + <Link + aria-label={intl.formatMessage(messages.public)} + title={intl.formatMessage(messages.public)} + to='/timelines/public' + ><Icon icon='globe' /></Link> + ))} + <a + aria-label={intl.formatMessage(messages.settings)} + onClick={onSettingsClick} + role='button' + title={intl.formatMessage(messages.settings)} + tabIndex='0' + ><Icon icon='cogs' /></a> + <a + aria-label={intl.formatMessage(messages.logout)} + data-method='delete' + href='/auth/sign_out' + title={intl.formatMessage(messages.logout)} + ><Icon icon='sign-out' /></a> + </nav> + ); +} + +DrawerHeader.propTypes = { + columns: ImmutablePropTypes.list, + intl: PropTypes.object, + onSettingsClick: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js index 8386ae47c..01ec18fc5 100644 --- a/app/javascript/flavours/glitch/features/drawer/index.js +++ b/app/javascript/flavours/glitch/features/drawer/index.js @@ -2,197 +2,147 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages } from 'react-intl'; -import spring from 'react-motion/lib/spring'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; // Actions. import { changeComposing } from 'flavours/glitch/actions/compose'; -import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { openModal } from 'flavours/glitch/actions/modal'; +import { + changeSearch, + clearSearch, + showSearch, + submitSearch, +} from 'flavours/glitch/actions/search'; // Components. -import Icon from 'flavours/glitch/components/icon'; -import Compose from 'flavours/glitch/features/compose'; -import NavigationContainer from './containers/navigation_container'; -import SearchContainer from './containers/search_container'; -import SearchResultsContainer from './containers/search_results_container'; +import DrawerHeader from './header'; +import DrawerPager from './pager'; +import DrawerResults from './results'; +import DrawerSearch from './search'; // Utils. -import Motion from 'flavours/glitch/util/optional_motion'; -import { - assignHandlers, - conditionalRender, -} from 'flavours/glitch/util/react_helpers'; - -// Messages. -const messages = defineMessages({ - community: { - defaultMessage: 'Local timeline', - id: 'navigation_bar.community_timeline', - }, - home_timeline: { - defaultMessage: 'Home', - id: 'tabs_bar.home', - }, - logout: { - defaultMessage: 'Logout', - id: 'navigation_bar.logout', - }, - notifications: { - defaultMessage: 'Notifications', - id: 'tabs_bar.notifications', - }, - public: { - defaultMessage: 'Federated timeline', - id: 'navigation_bar.public_timeline', - }, - settings: { - defaultMessage: 'App settings', - id: 'navigation_bar.app_settings', - }, - start: { - defaultMessage: 'Getting started', - id: 'getting_started.heading', - }, -}); +import { me } from 'flavours/glitch/util/initial_state'; +import { wrap } from 'flavours/glitch/util/redux_helpers'; // State mapping. const mapStateToProps = state => ({ + account: state.getIn(['accounts', me]), columns: state.getIn(['settings', 'columns']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), + isComposing: state.getIn(['compose', 'is_composing']), + results: state.getIn(['search', 'results']), + searchHidden: state.getIn(['search', 'hidden']), + searchValue: state.getIn(['search', 'value']), + submitted: state.getIn(['search', 'submitted']), }); // Dispatch mapping. const mapDispatchToProps = dispatch => ({ - onBlur () { + change (value) { + dispatch(changeSearch(value)); + }, + changeComposingOff () { dispatch(changeComposing(false)); }, - onFocus () { + changeComposingOn () { dispatch(changeComposing(true)); }, - onSettingsOpen () { + clear () { + dispatch(clearSearch()); + }, + show () { + dispatch(showSearch()); + }, + submit () { + dispatch(submitSearch()); + }, + openSettings () { dispatch(openModal('SETTINGS', {})); }, }); // The component. -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default function Drawer ({ - columns, - intl, - multiColumn, - onBlur, - onFocus, - onSettingsOpen, - showSearch, -}) { +class Drawer extends React.Component { - // Only renders the component if the column isn't being shown. - const renderForColumn = conditionalRender.bind( - columnId => !columns.some(column => column.get('id') === columnId) - ); + // Constructor. + constructor (props) { + super(props); + } - // The result. - return ( - <div className='drawer'> - {multiColumn ? ( - <nav className='drawer__header'> - <Link - aria-label={intl.formatMessage(messages.start)} - className='drawer__tab' - title={intl.formatMessage(messages.start)} - to='/getting-started' - ><Icon icon='asterisk' /></Link> - {renderForColumn('HOME', ( - <Link - aria-label={intl.formatMessage(messages.home_timeline)} - className='drawer__tab' - title={intl.formatMessage(messages.home_timeline)} - to='/timelines/home' - ><Icon icon='home' /></Link> - ))} - {renderForColumn('NOTIFICATIONS', ( - <Link - aria-label={intl.formatMessage(messages.notifications)} - className='drawer__tab' - title={intl.formatMessage(messages.notifications)} - to='/notifications' - ><Icon icon='bell' /></Link> - ))} - {renderForColumn('COMMUNITY', ( - <Link - aria-label={intl.formatMessage(messages.community)} - className='drawer__tab' - title={intl.formatMessage(messages.community)} - to='/timelines/public/local' - ><Icon icon='users' /></Link> - ))} - {renderForColumn('PUBLIC', ( - <Link - aria-label={intl.formatMessage(messages.public)} - className='drawer__tab' - title={intl.formatMessage(messages.public)} - to='/timelines/public' - ><Icon icon='globe' /></Link> - ))} - <a - aria-label={intl.formatMessage(messages.settings)} - className='drawer__tab' - onClick={settings} - role='button' - title={intl.formatMessage(messages.settings)} - tabIndex='0' - ><Icon icon='cogs' /></a> - <a - aria-label={intl.formatMessage(messages.logout)} - className='drawer__tab' - data-method='delete' - href='/auth/sign_out' - title={intl.formatMessage(messages.logout)} - ><Icon icon='sign-out' /></a> - </nav> - ) : null} - <SearchContainer /> - <div className='drawer__pager'> - <div - className='drawer__inner scrollable optionally-scrollable' - onFocus={focus} - > - <NavigationContainer onClose={blur} /> - <Compose /> - </div> - <Motion - defaultStyle={{ x: -100 }} - style={{ - x: spring(showSearch ? 0 : -100, { - stiffness: 210, - damping: 20, - }) - }} - > - {({ x }) => ( - <div - className='drawer__inner darker scrollable optionally-scrollable' - style={{ - transform: `translateX(${x}%)`, - visibility: x === -100 ? 'hidden' : 'visible' - }} - ><SearchResultsContainer /></div> - )} - </Motion> + // Rendering. + render () { + const { + dispatch: { + change, + changeComposingOff, + changeComposingOn, + clear, + openSettings, + show, + submit, + }, + intl, + multiColumn, + state: { + account, + columns, + isComposing, + results, + searchHidden, + searchValue, + submitted, + }, + } = this.props; + + // The result. + return ( + <div className='drawer'> + {multiColumn ? ( + <DrawerHeader + columns={columns} + intl={intl} + onSettingsClick={openSettings} + /> + ) : null} + <DrawerSearch + intl={intl} + onChange={change} + onClear={clear} + onShow={show} + onSubmit={submit} + submitted={submitted} + value={searchValue} + /> + <DrawerPager + account={account} + active={isComposing} + onBlur={changeComposingOff} + onFocus={changeComposingOn} + /> + <DrawerResults + results={results} + visible={submitted && !searchHidden} + /> </div> - </div> - ); + ); + } + } // Props. Drawer.propTypes = { dispatch: PropTypes.func.isRequired, - columns: ImmutablePropTypes.list.isRequired, - multiColumn: PropTypes.bool, - showSearch: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + state: PropTypes.shape({ + account: ImmutablePropTypes.map, + columns: ImmutablePropTypes.list, + isComposing: PropTypes.bool, + results: ImmutablePropTypes.map, + searchHidden: PropTypes.bool, + searchValue: PropTypes.string, + submitted: PropTypes.bool, + }).isRequired, }; + +// Connecting and export. +export { Drawer as WrappedComponent }; +export default wrap(Drawer, mapStateToProps, mapDispatchToProps, true); diff --git a/app/javascript/flavours/glitch/features/drawer/pager/account/index.js b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js new file mode 100644 index 000000000..2ee95d5b9 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js @@ -0,0 +1,70 @@ +// Package imports. +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; + +// Components. +import Avatar from 'flavours/glitch/components/avatar'; +import Permalink from 'flavours/glitch/components/permalink'; + +// Utils. +import { hiddenComponent } from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + edit: { + defaultMessage: 'Edit profile', + id: 'navigation_bar.edit_profile', + }, +}); + +// The component. +export default function DrawerPagerAccount ({ account }) { + + // We need an account to render. + if (!account) { + return ( + <div className='drawer--pager--account'> + <a + className='edit' + href='/settings/profile' + > + <FormattedMessage {...messages.edit} /> + </a> + </div> + ); + } + + // The result. + return ( + <div className='drawer--pager--account'> + <Permalink + className='avatar' + href={account.get('url')} + to={`/accounts/${account.get('id')}`} + > + <span {...hiddenComponent}>{account.get('acct')}</span> + <Avatar + account={account} + size={40} + /> + </Permalink> + <Permalink + className='acct' + href={account.get('url')} + to={`/accounts/${account.get('id')}`} + > + <strong>@{account.get('acct')}</strong> + </Permalink> + <a + className='edit' + href='/settings/profile' + ><FormattedMessage {...messages.edit} /></a> + </div> + ); +} + +DrawerPagerAccount.propTypes = { account: ImmutablePropTypes.map }; diff --git a/app/javascript/flavours/glitch/features/drawer/pager/index.js b/app/javascript/flavours/glitch/features/drawer/pager/index.js new file mode 100644 index 000000000..8dc2d3ee9 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/pager/index.js @@ -0,0 +1,43 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import Composer from 'flavours/glitch/features/composer'; +import DrawerPagerAccount from './account'; + +// The component. +export default function DrawerPager ({ + account, + active, + onClose, + onFocus, +}) { + const computedClass = classNames('drawer--pager', { active }); + + // The result. + return ( + <div + className={computedClass} + onFocus={onFocus} + > + <DrawerPagerAccount account={account} /> + <IconButton + icon='close' + onClick={onClose} + title='' + /> + <Composer /> + </div> + ); +} + +DrawerPager.propTypes = { + account: ImmutablePropTypes.map, + active: PropTypes.bool, + onClose: PropTypes.func, + onFocus: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js new file mode 100644 index 000000000..559d56da5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -0,0 +1,114 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import spring from 'react-motion/lib/spring'; +import { Link } from 'react-router-dom'; + +// Components. +import AccountContainer from 'flavours/glitch/containers/account_container'; +import StatusContainer from 'flavours/glitch/containers/status_container'; + +// Utils. +import Motion from 'flavours/glitch/util/optional_motion'; + +// Messages. +const messages = defineMessages({ + total: { + defaultMessage: '{count, number} {count, plural, one {result} other {results}}', + id: 'search_results.total', + }, +}); + +// The component. +export default function DrawerPager ({ + results, + visible, +}) { + const accounts = results ? results.get('accounts') : null; + const statuses = results ? results.get('statuses') : null; + const hashtags = results ? results.get('hashtags') : null; + + const count = [accounts, statuses, hashtags].reduce(function (size, item) { + if (item && item.size) { + return size + item.size; + } + return size; + }, 0); + + // The result. + return ( + <Motion + defaultStyle={{ x: -100 }} + style={{ + x: spring(visible ? 0 : -100, { + stiffness: 210, + damping: 20, + }), + }} + > + {({ x }) => ( + <div + className='drawer--results' + style={{ + transform: `translateX(${x}%)`, + visibility: x === -100 ? 'hidden' : 'visible', + }} + > + <header> + <FormattedMessage + {...messages.total} + values={{ count }} + /> + </header> + {accounts && accounts.size ? ( + <section> + {accounts.map( + accountId => ( + <AccountContainer + id={accountId} + key={accountId} + /> + ) + )} + </section> + ) : null} + {statuses && statuses.size ? ( + <section> + {statuses.map( + statusId => ( + <StatusContainer + id={statusId} + key={statusId} + /> + ) + )} + </section> + ) : null} + {hashtags && hashtags.size ? ( + <section> + {hashtags.map( + hashtag => ( + <Link + className='hashtag' + key={hashtag} + to={`/timelines/tag/${hashtag}`} + >#{hashtag}</Link> + ) + )} + </section> + ) : null} + </div> + )} + </Motion> + ); +} + +DrawerPager.propTypes = { + results: ImmutablePropTypes.map, + visible: PropTypes.bool, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js new file mode 100644 index 000000000..ccb2ba859 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/search/index.js @@ -0,0 +1,149 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import Overlay from 'react-overlays/lib/Overlay'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; +import DrawerSearchPopout from './popout'; + +// Utils. +import { focusRoot } from 'flavours/glitch/util/dom_helpers'; +import { + assignHandlers, + hiddenComponent, +} from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + placeholder: { + defaultMessage: 'Search', + id: 'search.placeholder', + }, +}); + +// Handlers. +const handlers = { + + blur () { + this.setState({ expanded: false }); + }, + + change ({ target: { value } }) { + const { onChange } = this.props; + if (onChange) { + onChange(value); + } + }, + + clear (e) { + const { + onClear, + submitted, + value: { length }, + } = this.props; + e.preventDefault(); // Prevents focus change ?? + if (onClear && (submitted || length)) { + onClear(); + } + }, + + focus () { + const { onShow } = this.props; + this.setState({ expanded: true }); + if (onShow) { + onShow(); + } + }, + + keyUp (e) { + const { onSubmit } = this.props; + switch (e.key) { + case 'Enter': + if (onSubmit) { + onSubmit(); + } + break; + case 'Escape': + focusRoot(); + } + }, +}; + +// The component. +export default class DrawerSearch extends React.PureComponent { + + constructor (props) { + super(props); + assignHandlers(this, handlers); + this.state = { expanded: false }; + } + + render () { + const { + blur, + change, + clear, + focus, + keyUp, + } = this.handlers; + const { + intl, + submitted, + value, + } = this.props; + const { expanded } = this.state; + const computedClass = classNames('drawer--search', { active: value.length || submitted }); + + return ( + <div className={computedClass}> + <label> + <span {...hiddenComponent}> + <FormattedMessage {...messages.placeholder} /> + </span> + <input + type='text' + placeholder={intl.formatMessage(messages.placeholder)} + value={value} + onChange={change} + onKeyUp={keyUp} + onFocus={focus} + onBlur={blur} + /> + </label> + <div + aria-label={intl.formatMessage(messages.placeholder)} + className='icon' + onClick={clear} + role='button' + tabIndex='0' + > + <Icon icon='search' /> + <Icon icon='fa-times-circle' /> + </div> + + <Overlay + placement='bottom' + show={expanded && !value.length && !submitted} + target={this} + ><DrawerSearchPopout /></Overlay> + </div> + ); + } + +} + +DrawerSearch.propTypes = { + value: PropTypes.string, + submitted: PropTypes.bool, + onChange: PropTypes.func, + onSubmit: PropTypes.func, + onClear: PropTypes.func, + onShow: PropTypes.func, + intl: PropTypes.object, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js new file mode 100644 index 000000000..bd36275f5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js @@ -0,0 +1,95 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import spring from 'react-motion/lib/spring'; + +// Utils. +import Motion from 'flavours/glitch/util/optional_motion'; + +// Messages. +const messages = defineMessages({ + format: { + defaultMessage: 'Advanced search format', + id: 'search_popout.search_format', + }, + hashtag: { + defaultMessage: 'hashtag', + id: 'search_popout.tips.hashtag', + }, + status: { + defaultMessage: 'status', + id: 'search_popout.tips.status', + }, + text: { + defaultMessage: 'Simple text returns matching display names, usernames and hashtags', + id: 'search_popout.tips.text', + }, + user: { + defaultMessage: 'user', + id: 'search_popout.tips.user', + }, +}); + +const motionSpring = spring(1, { damping: 35, stiffness: 400 }); + +export default function DrawerSearchPopout ({ style }) { + return ( + <Motion + defaultStyle={{ + opacity: 0, + scaleX: 0.85, + scaleY: 0.75, + }} + style={{ + opacity: motionSpring, + scaleX: motionSpring, + scaleY: motionSpring, + }} + > + {({ opacity, scaleX, scaleY }) => ( + <div + className='drawer--search--popout' + style={{ + ...style, + position: 'absolute', + width: 285, + opacity: opacity, + transform: `scale(${scaleX}, ${scaleY})`, + }} + > + <h4><FormattedMessage {...messages.format} /></h4> + <ul> + <li> + <em>#example</em> + {' '} + <FormattedMessage {...messages.hashtag} /> + </li> + <li> + <em>@username@domain</em> + {' '} + <FormattedMessage {...messages.user} /> + </li> + <li> + <em>URL</em> + {' '} + <FormattedMessage {...messages.user} /> + </li> + <li> + <em>URL</em> + {' '} + <FormattedMessage {...messages.status} /> + </li> + </ul> + <FormattedMessage {...messages.text} /> + </div> + )} + </Motion> + ); +} + +// Props. +DrawerSearchPopout.propTypes = { style: PropTypes.object }; diff --git a/app/javascript/flavours/glitch/util/dom_helpers.js b/app/javascript/flavours/glitch/util/dom_helpers.js index ee95ef8dd..3e1f4a26d 100644 --- a/app/javascript/flavours/glitch/util/dom_helpers.js +++ b/app/javascript/flavours/glitch/util/dom_helpers.js @@ -4,3 +4,11 @@ import detectPassiveEvents from 'detect-passive-events'; // This will either be a passive lister options object (if passive // events are supported), or `false`. export const withPassive = detectPassiveEvents.hasSupport ? { passive: true } : false; + +// Focuses the root element. +export function focusRoot () { + let e; + if (document && (e = document.querySelector('.ui')) && (e = e.parentElement)) { + e.focus(); + } +} diff --git a/app/javascript/flavours/glitch/util/react_helpers.js b/app/javascript/flavours/glitch/util/react_helpers.js index 0826f3584..087e3969d 100644 --- a/app/javascript/flavours/glitch/util/react_helpers.js +++ b/app/javascript/flavours/glitch/util/react_helpers.js @@ -14,7 +14,7 @@ export function assignHandlers (target, handlers) { // This function only returns the component if the result of calling // `test` with `data` is `true`. Useful with funciton binding. export function conditionalRender (test, data, component) { - return test ? component : null; + return test(data) ? component : null; } // This object provides props to make the component not visible. diff --git a/app/javascript/flavours/glitch/util/redux_helpers.js b/app/javascript/flavours/glitch/util/redux_helpers.js index 3bc8bc86f..c0f5eeb28 100644 --- a/app/javascript/flavours/glitch/util/redux_helpers.js +++ b/app/javascript/flavours/glitch/util/redux_helpers.js @@ -1,3 +1,6 @@ +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; + // Merges react-redux props. export function mergeProps (stateProps, dispatchProps, ownProps) { Object.assign({}, ownProps, { @@ -5,3 +8,9 @@ export function mergeProps (stateProps, dispatchProps, ownProps) { state: Object.assign({}, stateProps, ownProps.state || {}), }); } + +// Connects a component. +export function wrap (Component, mapStateToProps, mapDispatchToProps, options) { + const withIntl = typeof options === 'object' ? options.withIntl : !!options; + return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component)); +} |