mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-16 23:22:01 +00:00
WIP <Compose> Refactor; <Drawer> ed.
This commit is contained in:
parent
924ffe81d4
commit
3c29f57404
|
@ -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);
|
||||
|
|
|
@ -70,7 +70,7 @@ const handlers = {
|
|||
// dropdown.
|
||||
if (onModalClose && isUserTouching()) {
|
||||
if (open) {
|
||||
onModalClose()
|
||||
onModalClose();
|
||||
} else if (onChange && onModalOpen) {
|
||||
onModalOpen({
|
||||
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 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);
|
||||
|
|
|
@ -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
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue