mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-27 22:01:35 +01:00
WIP <Compose> Refactor; <Drawer> ed.
This commit is contained in:
parent
924ffe81d4
commit
3c29f57404
18 changed files with 723 additions and 455 deletions
|
@ -2,9 +2,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
|
|
||||||
// Actions.
|
// Actions.
|
||||||
import {
|
import {
|
||||||
|
@ -43,7 +40,7 @@ import { countableText } from 'flavours/glitch/util/counter';
|
||||||
import { me } from 'flavours/glitch/util/initial_state';
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
import { isMobile } from 'flavours/glitch/util/is_mobile';
|
import { isMobile } from 'flavours/glitch/util/is_mobile';
|
||||||
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
|
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.
|
// State mapping.
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
|
@ -204,9 +201,7 @@ const handlers = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The component.
|
// The component.
|
||||||
@injectIntl
|
class Composer extends React.Component {
|
||||||
@connect(mapStateToProps, mapDispatchToProps, mergeProps)
|
|
||||||
export default class Composer extends React.Component {
|
|
||||||
|
|
||||||
// Constructor.
|
// Constructor.
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
|
@ -408,7 +403,7 @@ export default class Composer extends React.Component {
|
||||||
// Context
|
// Context
|
||||||
Composer.contextTypes = {
|
Composer.contextTypes = {
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
Composer.propTypes = {
|
Composer.propTypes = {
|
||||||
|
@ -438,3 +433,7 @@ Composer.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Connecting and export.
|
||||||
|
export { Composer as WrappedComponent };
|
||||||
|
export default wrap(Composer, mapStateToProps, mapDispatchToProps, true);
|
||||||
|
|
|
@ -70,7 +70,7 @@ const handlers = {
|
||||||
// dropdown.
|
// dropdown.
|
||||||
if (onModalClose && isUserTouching()) {
|
if (onModalClose && isUserTouching()) {
|
||||||
if (open) {
|
if (open) {
|
||||||
onModalClose()
|
onModalClose();
|
||||||
} else if (onChange && onModalOpen) {
|
} else if (onChange && onModalOpen) {
|
||||||
onModalOpen({
|
onModalOpen({
|
||||||
actions: items.map(
|
actions: items.map(
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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);
|
|
|
@ -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);
|
|
117
app/javascript/flavours/glitch/features/drawer/header/index.js
Normal file
117
app/javascript/flavours/glitch/features/drawer/header/index.js
Normal file
|
@ -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,
|
||||||
|
};
|
|
@ -2,197 +2,147 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
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.
|
// Actions.
|
||||||
import { changeComposing } from 'flavours/glitch/actions/compose';
|
import { changeComposing } from 'flavours/glitch/actions/compose';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import {
|
||||||
|
changeSearch,
|
||||||
|
clearSearch,
|
||||||
|
showSearch,
|
||||||
|
submitSearch,
|
||||||
|
} from 'flavours/glitch/actions/search';
|
||||||
|
|
||||||
// Components.
|
// Components.
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import DrawerHeader from './header';
|
||||||
import Compose from 'flavours/glitch/features/compose';
|
import DrawerPager from './pager';
|
||||||
import NavigationContainer from './containers/navigation_container';
|
import DrawerResults from './results';
|
||||||
import SearchContainer from './containers/search_container';
|
import DrawerSearch from './search';
|
||||||
import SearchResultsContainer from './containers/search_results_container';
|
|
||||||
|
|
||||||
// Utils.
|
// Utils.
|
||||||
import Motion from 'flavours/glitch/util/optional_motion';
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
import {
|
import { wrap } from 'flavours/glitch/util/redux_helpers';
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// State mapping.
|
// State mapping.
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
account: state.getIn(['accounts', me]),
|
||||||
columns: state.getIn(['settings', 'columns']),
|
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.
|
// Dispatch mapping.
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onBlur () {
|
change (value) {
|
||||||
|
dispatch(changeSearch(value));
|
||||||
|
},
|
||||||
|
changeComposingOff () {
|
||||||
dispatch(changeComposing(false));
|
dispatch(changeComposing(false));
|
||||||
},
|
},
|
||||||
onFocus () {
|
changeComposingOn () {
|
||||||
dispatch(changeComposing(true));
|
dispatch(changeComposing(true));
|
||||||
},
|
},
|
||||||
onSettingsOpen () {
|
clear () {
|
||||||
|
dispatch(clearSearch());
|
||||||
|
},
|
||||||
|
show () {
|
||||||
|
dispatch(showSearch());
|
||||||
|
},
|
||||||
|
submit () {
|
||||||
|
dispatch(submitSearch());
|
||||||
|
},
|
||||||
|
openSettings () {
|
||||||
dispatch(openModal('SETTINGS', {}));
|
dispatch(openModal('SETTINGS', {}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The component.
|
// The component.
|
||||||
@connect(mapStateToProps, mapDispatchToProps)
|
class Drawer extends React.Component {
|
||||||
@injectIntl
|
|
||||||
export default function Drawer ({
|
// Constructor.
|
||||||
columns,
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering.
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
dispatch: {
|
||||||
|
change,
|
||||||
|
changeComposingOff,
|
||||||
|
changeComposingOn,
|
||||||
|
clear,
|
||||||
|
openSettings,
|
||||||
|
show,
|
||||||
|
submit,
|
||||||
|
},
|
||||||
intl,
|
intl,
|
||||||
multiColumn,
|
multiColumn,
|
||||||
onBlur,
|
state: {
|
||||||
onFocus,
|
account,
|
||||||
onSettingsOpen,
|
columns,
|
||||||
showSearch,
|
isComposing,
|
||||||
}) {
|
results,
|
||||||
|
searchHidden,
|
||||||
// Only renders the component if the column isn't being shown.
|
searchValue,
|
||||||
const renderForColumn = conditionalRender.bind(
|
submitted,
|
||||||
columnId => !columns.some(column => column.get('id') === columnId)
|
},
|
||||||
);
|
} = this.props;
|
||||||
|
|
||||||
// The result.
|
// The result.
|
||||||
return (
|
return (
|
||||||
<div className='drawer'>
|
<div className='drawer'>
|
||||||
{multiColumn ? (
|
{multiColumn ? (
|
||||||
<nav className='drawer__header'>
|
<DrawerHeader
|
||||||
<Link
|
columns={columns}
|
||||||
aria-label={intl.formatMessage(messages.start)}
|
intl={intl}
|
||||||
className='drawer__tab'
|
onSettingsClick={openSettings}
|
||||||
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}
|
) : null}
|
||||||
<SearchContainer />
|
<DrawerSearch
|
||||||
<div className='drawer__pager'>
|
intl={intl}
|
||||||
<div
|
onChange={change}
|
||||||
className='drawer__inner scrollable optionally-scrollable'
|
onClear={clear}
|
||||||
onFocus={focus}
|
onShow={show}
|
||||||
>
|
onSubmit={submit}
|
||||||
<NavigationContainer onClose={blur} />
|
submitted={submitted}
|
||||||
<Compose />
|
value={searchValue}
|
||||||
</div>
|
/>
|
||||||
<Motion
|
<DrawerPager
|
||||||
defaultStyle={{ x: -100 }}
|
account={account}
|
||||||
style={{
|
active={isComposing}
|
||||||
x: spring(showSearch ? 0 : -100, {
|
onBlur={changeComposingOff}
|
||||||
stiffness: 210,
|
onFocus={changeComposingOn}
|
||||||
damping: 20,
|
/>
|
||||||
})
|
<DrawerResults
|
||||||
}}
|
results={results}
|
||||||
>
|
visible={submitted && !searchHidden}
|
||||||
{({ x }) => (
|
/>
|
||||||
<div
|
|
||||||
className='drawer__inner darker scrollable optionally-scrollable'
|
|
||||||
style={{
|
|
||||||
transform: `translateX(${x}%)`,
|
|
||||||
visibility: x === -100 ? 'hidden' : 'visible'
|
|
||||||
}}
|
|
||||||
><SearchResultsContainer /></div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
Drawer.propTypes = {
|
Drawer.propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columns: ImmutablePropTypes.list.isRequired,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
showSearch: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
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);
|
||||||
|
|
|
@ -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 };
|
|
@ -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,
|
||||||
|
};
|
114
app/javascript/flavours/glitch/features/drawer/results/index.js
Normal file
114
app/javascript/flavours/glitch/features/drawer/results/index.js
Normal file
|
@ -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,
|
||||||
|
};
|
149
app/javascript/flavours/glitch/features/drawer/search/index.js
Normal file
149
app/javascript/flavours/glitch/features/drawer/search/index.js
Normal file
|
@ -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,
|
||||||
|
};
|
|
@ -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 };
|
|
@ -4,3 +4,11 @@ import detectPassiveEvents from 'detect-passive-events';
|
||||||
// This will either be a passive lister options object (if passive
|
// This will either be a passive lister options object (if passive
|
||||||
// events are supported), or `false`.
|
// events are supported), or `false`.
|
||||||
export const withPassive = detectPassiveEvents.hasSupport ? { passive: true } : 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function assignHandlers (target, handlers) {
|
||||||
// This function only returns the component if the result of calling
|
// This function only returns the component if the result of calling
|
||||||
// `test` with `data` is `true`. Useful with funciton binding.
|
// `test` with `data` is `true`. Useful with funciton binding.
|
||||||
export function conditionalRender (test, data, component) {
|
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.
|
// This object provides props to make the component not visible.
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
// Merges react-redux props.
|
// Merges react-redux props.
|
||||||
export function mergeProps (stateProps, dispatchProps, ownProps) {
|
export function mergeProps (stateProps, dispatchProps, ownProps) {
|
||||||
Object.assign({}, ownProps, {
|
Object.assign({}, ownProps, {
|
||||||
|
@ -5,3 +8,9 @@ export function mergeProps (stateProps, dispatchProps, ownProps) {
|
||||||
state: Object.assign({}, stateProps, ownProps.state || {}),
|
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));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue