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 {
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) {
};
// The component.
-@injectIntl
-@connect(mapStateToProps, mapDispatchToProps, mergeProps)
-export default class Composer extends React.Component {
+class Composer extends React.Component {
// Constructor.
constructor (props) {
// Context
Composer.contextTypes = {
history: PropTypes.object,
-}
+};
// Props.
Composer.propTypes = {
text: PropTypes.string,
}).isRequired,
};
+
+// Connecting and export.
+export { Composer as WrappedComponent };
+export default wrap(Composer, mapStateToProps, mapDispatchToProps, true);
// dropdown.
if (onModalClose && isUserTouching()) {
if (open) {
- onModalClose()
+ onModalClose();
} else if (onChange && onModalOpen) {
onModalOpen({
actions: items.map(
+++ /dev/null
-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>
- );
- }
-
-}
+++ /dev/null
-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>
- );
- }
-
-}
+++ /dev/null
-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>
- );
- }
-
-}
+++ /dev/null
-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);
+++ /dev/null
-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);
+++ /dev/null
-import { connect } from 'react-redux';
-import SearchResults from '../components/search_results';
-
-const mapStateToProps = state => ({
- results: state.getIn(['search', 'results']),
-});
-
-export default connect(mapStateToProps)(SearchResults);
--- /dev/null
+// 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,
+};
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);
--- /dev/null
+// 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 };
--- /dev/null
+// 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,
+};
--- /dev/null
+// 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,
+};
--- /dev/null
+// 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,
+};
--- /dev/null
+// 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 };
// 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();
+ }
+}
// 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.
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
// Merges react-redux props.
export function mergeProps (stateProps, dispatchProps, ownProps) {
Object.assign({}, 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));
+}