From 924ffe81d477a8cf890c8117efb94b908760bccc Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sat, 23 Dec 2017 22:16:45 -0800 Subject: WIPgit status Refactor; ed. --- .../features/composer/options/dropdown/index.js | 243 +++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/index.js (limited to 'app/javascript/flavours/glitch/features/composer/options/dropdown/index.js') diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js new file mode 100644 index 000000000..0f304bc88 --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -0,0 +1,243 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/lib/Overlay'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import ComposerOptionsDropdownItem from './item'; + +// Utils. +import { withPassive } from 'flavours/glitch/util/dom_helpers'; +import { isUserTouching } from 'flavours/glitch/util/is_mobile'; +import Motion from 'flavours/glitch/util/optional_motion'; +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; + +// We'll use this to define our various transitions. +const springMotion = spring(1, { + damping: 35, + stiffness: 400, +}); + +// Handlers. +const handlers = { + + // Closes the dropdown. + close () { + this.setState({ open: false }); + }, + + // When the document is clicked elsewhere, we close the dropdown. + documentClick ({ target }) { + const { node } = this; + const { onClose } = this.props; + if (onClose && node && !node.contains(target)) { + onClose(); + } + }, + + // The enter key toggles the dropdown's open state, and the escape + // key closes it. + keyDown ({ key }) { + const { + close, + toggle, + } = this.handlers; + switch (key) { + case 'Enter': + toggle(); + break; + case 'Escape': + close(); + break; + } + }, + + // Toggles opening and closing the dropdown. + toggle () { + const { + items, + onChange, + onModalClose, + onModalOpen, + value, + } = this.props; + const { open } = this.state; + + // If this is a touch device, we open a modal instead of the + // dropdown. + if (onModalClose && isUserTouching()) { + if (open) { + onModalClose() + } else if (onChange && onModalOpen) { + onModalOpen({ + actions: items.map( + ({ + name, + ...rest + }) => ({ + ...rest, + active: value && name === value, + onClick (e) { + e.preventDefault(); // Prevents focus from changing + onModalClose(); + onChange(name); + }, + }) + ), + }); + } + + // Otherwise, we just set our state to open. + } else { + this.setState({ open: !open }); + } + }, + + // Stores our node in `this.node`. + ref (node) { + this.node = node; + }, +}; + +// The component. +export default class ComposerOptionsDropdown extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + this.state = { open: false }; + + // Instance variables. + this.node = null; + } + + // On mounting, we add our listeners. + componentDidMount () { + const { documentClick } = this.handlers; + document.addEventListener('click', documentClick, false); + document.addEventListener('touchend', documentClick, withPassive); + } + + // On unmounting, we remove our listeners. + componentWillUnmount () { + const { documentClick } = this.handlers; + document.removeEventListener('click', documentClick, false); + document.removeEventListener('touchend', documentClick, withPassive); + } + + // Rendering. + render () { + const { + close, + keyDown, + ref, + toggle, + } = this.handlers; + const { + active, + disabled, + title, + icon, + items, + onChange, + value, + } = this.props; + const { open } = this.state; + const computedClass = classNames('composer--options--dropdown', { + active, + open: open || active, + }); + + // The result. + return ( +
+ + + + {({ opacity, scaleX, scaleY }) => ( +
+ {items.map( + ({ + name, + ...rest + }) => ( + + ) + )} +
+ )} +
+
+
+ ); + } + +} + +// Props. +ComposerOptionsDropdown.propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + icon: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string.isRequired, + on: PropTypes.bool, + text: PropTypes.node, + })).isRequired, + onChange: PropTypes.func, + onModalClose: PropTypes.func, + onModalOpen: PropTypes.func, + title: PropTypes.string, + value: PropTypes.string, +}; -- cgit From 3c29f5740447270a4122b334281a907ecbdd4165 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Tue, 26 Dec 2017 16:54:28 -0800 Subject: WIP Refactor; ed. --- .../flavours/glitch/features/composer/index.js | 15 +- .../features/composer/options/dropdown/index.js | 2 +- .../features/drawer/components/navigation_bar.js | 38 --- .../glitch/features/drawer/components/search.js | 129 ---------- .../features/drawer/components/search_results.js | 65 ----- .../drawer/containers/navigation_container.js | 11 - .../features/drawer/containers/search_container.js | 35 --- .../drawer/containers/search_results_container.js | 8 - .../glitch/features/drawer/header/index.js | 117 +++++++++ .../flavours/glitch/features/drawer/index.js | 268 +++++++++------------ .../glitch/features/drawer/pager/account/index.js | 70 ++++++ .../flavours/glitch/features/drawer/pager/index.js | 43 ++++ .../glitch/features/drawer/results/index.js | 114 +++++++++ .../glitch/features/drawer/search/index.js | 149 ++++++++++++ .../glitch/features/drawer/search/popout/index.js | 95 ++++++++ app/javascript/flavours/glitch/util/dom_helpers.js | 8 + .../flavours/glitch/util/react_helpers.js | 2 +- .../flavours/glitch/util/redux_helpers.js | 9 + 18 files changed, 723 insertions(+), 455 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js delete mode 100644 app/javascript/flavours/glitch/features/drawer/components/search.js delete mode 100644 app/javascript/flavours/glitch/features/drawer/components/search_results.js delete mode 100644 app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js delete mode 100644 app/javascript/flavours/glitch/features/drawer/containers/search_container.js delete mode 100644 app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js create mode 100644 app/javascript/flavours/glitch/features/drawer/header/index.js create mode 100644 app/javascript/flavours/glitch/features/drawer/pager/account/index.js create mode 100644 app/javascript/flavours/glitch/features/drawer/pager/index.js create mode 100644 app/javascript/flavours/glitch/features/drawer/results/index.js create mode 100644 app/javascript/flavours/glitch/features/drawer/search/index.js create mode 100644 app/javascript/flavours/glitch/features/drawer/search/popout/index.js (limited to 'app/javascript/flavours/glitch/features/composer/options/dropdown/index.js') diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index 25c2622d8..506c668a7 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -2,9 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; // Actions. import { @@ -43,7 +40,7 @@ import { countableText } from 'flavours/glitch/util/counter'; import { me } from 'flavours/glitch/util/initial_state'; import { isMobile } from 'flavours/glitch/util/is_mobile'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -import { mergeProps } from 'flavours/glitch/util/redux_helpers'; +import { wrap } from 'flavours/glitch/util/redux_helpers'; // State mapping. function mapStateToProps (state) { @@ -204,9 +201,7 @@ const handlers = { }; // The component. -@injectIntl -@connect(mapStateToProps, mapDispatchToProps, mergeProps) -export default class Composer extends React.Component { +class Composer extends React.Component { // Constructor. constructor (props) { @@ -408,7 +403,7 @@ export default class Composer extends React.Component { // Context Composer.contextTypes = { history: PropTypes.object, -} +}; // Props. Composer.propTypes = { @@ -438,3 +433,7 @@ Composer.propTypes = { text: PropTypes.string, }).isRequired, }; + +// Connecting and export. +export { Composer as WrappedComponent }; +export default wrap(Composer, mapStateToProps, mapDispatchToProps, true); diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js index 0f304bc88..ee52008a7 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -70,7 +70,7 @@ const handlers = { // dropdown. if (onModalClose && isUserTouching()) { if (open) { - onModalClose() + onModalClose(); } else if (onChange && onModalOpen) { onModalOpen({ actions: items.map( diff --git a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js b/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js deleted file mode 100644 index 1b6d74123..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'flavours/glitch/components/avatar'; -import IconButton from 'flavours/glitch/components/icon_button'; -import Permalink from 'flavours/glitch/components/permalink'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class NavigationBar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render () { - return ( -
- - {this.props.account.get('acct')} - - - -
- - @{this.props.account.get('acct')} - - - -
- - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/components/search.js b/app/javascript/flavours/glitch/features/drawer/components/search.js deleted file mode 100644 index 1ce66b19d..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/search.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, -}); - -class SearchPopout extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - }; - - render () { - const { style } = this.props; - - return ( -
- - {({ opacity, scaleX, scaleY }) => ( -
-

- -
    -
  • #example
  • -
  • @username@domain
  • -
  • URL
  • -
  • URL
  • -
- - -
- )} -
-
- ); - } - -} - -@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 ( -
- - -
- - -
- - - - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/components/search_results.js b/app/javascript/flavours/glitch/features/drawer/components/search_results.js deleted file mode 100644 index 2a4818d4e..000000000 --- a/app/javascript/flavours/glitch/features/drawer/components/search_results.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import StatusContainer from 'flavours/glitch/containers/status_container'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class SearchResults extends ImmutablePureComponent { - - static propTypes = { - results: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { results } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( -
- {results.get('accounts').map(accountId => )} -
- ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( -
- {results.get('statuses').map(statusId => )} -
- ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( -
- {results.get('hashtags').map(hashtag => - - #{hashtag} - - )} -
- ); - } - - return ( -
-
- -
- - {accounts} - {statuses} - {hashtags} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js b/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js deleted file mode 100644 index eb630ffbb..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux'; -import NavigationBar from '../components/navigation_bar'; -import { me } from 'flavours/glitch/util/initial_state'; - -const mapStateToProps = state => { - return { - account: state.getIn(['accounts', me]), - }; -}; - -export default connect(mapStateToProps)(NavigationBar); diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_container.js deleted file mode 100644 index 8f4bfcf08..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { - changeSearch, - clearSearch, - submitSearch, - showSearch, -} from 'flavours/glitch/actions/search'; -import Search from '../components/search'; - -const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']), - submitted: state.getIn(['search', 'submitted']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeSearch(value)); - }, - - onClear () { - dispatch(clearSearch()); - }, - - onSubmit () { - dispatch(submitSearch()); - }, - - onShow () { - dispatch(showSearch()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js deleted file mode 100644 index 16d95d417..000000000 --- a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import SearchResults from '../components/search_results'; - -const mapStateToProps = state => ({ - results: state.getIn(['search', 'results']), -}); - -export default connect(mapStateToProps)(SearchResults); diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js new file mode 100644 index 000000000..fd79b6e18 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/header/index.js @@ -0,0 +1,117 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages } from 'react-intl'; +import { Link } from 'react-router-dom'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { conditionalRender } from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + community: { + defaultMessage: 'Local timeline', + id: 'navigation_bar.community_timeline', + }, + home_timeline: { + defaultMessage: 'Home', + id: 'tabs_bar.home', + }, + logout: { + defaultMessage: 'Logout', + id: 'navigation_bar.logout', + }, + notifications: { + defaultMessage: 'Notifications', + id: 'tabs_bar.notifications', + }, + public: { + defaultMessage: 'Federated timeline', + id: 'navigation_bar.public_timeline', + }, + settings: { + defaultMessage: 'App settings', + id: 'navigation_bar.app_settings', + }, + start: { + defaultMessage: 'Getting started', + id: 'getting_started.heading', + }, +}); + +// The component. +export default function DrawerHeader ({ + columns, + intl, + onSettingsClick, +}) { + + // Only renders the component if the column isn't being shown. + const renderForColumn = conditionalRender.bind( + columnId => !columns || !columns.some( + column => column.get('id') === columnId + ) + ); + + // The result. + return ( + + ); +} + +DrawerHeader.propTypes = { + columns: ImmutablePropTypes.list, + intl: PropTypes.object, + onSettingsClick: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js index 8386ae47c..01ec18fc5 100644 --- a/app/javascript/flavours/glitch/features/drawer/index.js +++ b/app/javascript/flavours/glitch/features/drawer/index.js @@ -2,197 +2,147 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages } from 'react-intl'; -import spring from 'react-motion/lib/spring'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; // Actions. import { changeComposing } from 'flavours/glitch/actions/compose'; -import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { openModal } from 'flavours/glitch/actions/modal'; +import { + changeSearch, + clearSearch, + showSearch, + submitSearch, +} from 'flavours/glitch/actions/search'; // Components. -import Icon from 'flavours/glitch/components/icon'; -import Compose from 'flavours/glitch/features/compose'; -import NavigationContainer from './containers/navigation_container'; -import SearchContainer from './containers/search_container'; -import SearchResultsContainer from './containers/search_results_container'; +import DrawerHeader from './header'; +import DrawerPager from './pager'; +import DrawerResults from './results'; +import DrawerSearch from './search'; // Utils. -import Motion from 'flavours/glitch/util/optional_motion'; -import { - assignHandlers, - conditionalRender, -} from 'flavours/glitch/util/react_helpers'; - -// Messages. -const messages = defineMessages({ - community: { - defaultMessage: 'Local timeline', - id: 'navigation_bar.community_timeline', - }, - home_timeline: { - defaultMessage: 'Home', - id: 'tabs_bar.home', - }, - logout: { - defaultMessage: 'Logout', - id: 'navigation_bar.logout', - }, - notifications: { - defaultMessage: 'Notifications', - id: 'tabs_bar.notifications', - }, - public: { - defaultMessage: 'Federated timeline', - id: 'navigation_bar.public_timeline', - }, - settings: { - defaultMessage: 'App settings', - id: 'navigation_bar.app_settings', - }, - start: { - defaultMessage: 'Getting started', - id: 'getting_started.heading', - }, -}); +import { me } from 'flavours/glitch/util/initial_state'; +import { wrap } from 'flavours/glitch/util/redux_helpers'; // State mapping. const mapStateToProps = state => ({ + account: state.getIn(['accounts', me]), columns: state.getIn(['settings', 'columns']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), + isComposing: state.getIn(['compose', 'is_composing']), + results: state.getIn(['search', 'results']), + searchHidden: state.getIn(['search', 'hidden']), + searchValue: state.getIn(['search', 'value']), + submitted: state.getIn(['search', 'submitted']), }); // Dispatch mapping. const mapDispatchToProps = dispatch => ({ - onBlur () { + change (value) { + dispatch(changeSearch(value)); + }, + changeComposingOff () { dispatch(changeComposing(false)); }, - onFocus () { + changeComposingOn () { dispatch(changeComposing(true)); }, - onSettingsOpen () { + clear () { + dispatch(clearSearch()); + }, + show () { + dispatch(showSearch()); + }, + submit () { + dispatch(submitSearch()); + }, + openSettings () { dispatch(openModal('SETTINGS', {})); }, }); // The component. -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default function Drawer ({ - columns, - intl, - multiColumn, - onBlur, - onFocus, - onSettingsOpen, - showSearch, -}) { +class Drawer extends React.Component { - // Only renders the component if the column isn't being shown. - const renderForColumn = conditionalRender.bind( - columnId => !columns.some(column => column.get('id') === columnId) - ); + // Constructor. + constructor (props) { + super(props); + } - // The result. - return ( -
- {multiColumn ? ( - - ) : null} - -
-
- - -
- - {({ x }) => ( -
- )} -
+ // 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 ( +
+ {multiColumn ? ( + + ) : null} + + +
-
- ); + ); + } + } // Props. Drawer.propTypes = { dispatch: PropTypes.func.isRequired, - columns: ImmutablePropTypes.list.isRequired, - multiColumn: PropTypes.bool, - showSearch: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + state: PropTypes.shape({ + account: ImmutablePropTypes.map, + columns: ImmutablePropTypes.list, + isComposing: PropTypes.bool, + results: ImmutablePropTypes.map, + searchHidden: PropTypes.bool, + searchValue: PropTypes.string, + submitted: PropTypes.bool, + }).isRequired, }; + +// Connecting and export. +export { Drawer as WrappedComponent }; +export default wrap(Drawer, mapStateToProps, mapDispatchToProps, true); diff --git a/app/javascript/flavours/glitch/features/drawer/pager/account/index.js b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js new file mode 100644 index 000000000..2ee95d5b9 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js @@ -0,0 +1,70 @@ +// Package imports. +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; + +// Components. +import Avatar from 'flavours/glitch/components/avatar'; +import Permalink from 'flavours/glitch/components/permalink'; + +// Utils. +import { hiddenComponent } from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + edit: { + defaultMessage: 'Edit profile', + id: 'navigation_bar.edit_profile', + }, +}); + +// The component. +export default function DrawerPagerAccount ({ account }) { + + // We need an account to render. + if (!account) { + return ( +
+ + + +
+ ); + } + + // The result. + return ( +
+ + {account.get('acct')} + + + + @{account.get('acct')} + + +
+ ); +} + +DrawerPagerAccount.propTypes = { account: ImmutablePropTypes.map }; diff --git a/app/javascript/flavours/glitch/features/drawer/pager/index.js b/app/javascript/flavours/glitch/features/drawer/pager/index.js new file mode 100644 index 000000000..8dc2d3ee9 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/pager/index.js @@ -0,0 +1,43 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import Composer from 'flavours/glitch/features/composer'; +import DrawerPagerAccount from './account'; + +// The component. +export default function DrawerPager ({ + account, + active, + onClose, + onFocus, +}) { + const computedClass = classNames('drawer--pager', { active }); + + // The result. + return ( +
+ + + +
+ ); +} + +DrawerPager.propTypes = { + account: ImmutablePropTypes.map, + active: PropTypes.bool, + onClose: PropTypes.func, + onFocus: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js new file mode 100644 index 000000000..559d56da5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -0,0 +1,114 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import spring from 'react-motion/lib/spring'; +import { Link } from 'react-router-dom'; + +// Components. +import AccountContainer from 'flavours/glitch/containers/account_container'; +import StatusContainer from 'flavours/glitch/containers/status_container'; + +// Utils. +import Motion from 'flavours/glitch/util/optional_motion'; + +// Messages. +const messages = defineMessages({ + total: { + defaultMessage: '{count, number} {count, plural, one {result} other {results}}', + id: 'search_results.total', + }, +}); + +// The component. +export default function DrawerPager ({ + results, + visible, +}) { + const accounts = results ? results.get('accounts') : null; + const statuses = results ? results.get('statuses') : null; + const hashtags = results ? results.get('hashtags') : null; + + const count = [accounts, statuses, hashtags].reduce(function (size, item) { + if (item && item.size) { + return size + item.size; + } + return size; + }, 0); + + // The result. + return ( + + {({ x }) => ( +
+
+ +
+ {accounts && accounts.size ? ( +
+ {accounts.map( + accountId => ( + + ) + )} +
+ ) : null} + {statuses && statuses.size ? ( +
+ {statuses.map( + statusId => ( + + ) + )} +
+ ) : null} + {hashtags && hashtags.size ? ( +
+ {hashtags.map( + hashtag => ( + #{hashtag} + ) + )} +
+ ) : null} +
+ )} +
+ ); +} + +DrawerPager.propTypes = { + results: ImmutablePropTypes.map, + visible: PropTypes.bool, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js new file mode 100644 index 000000000..ccb2ba859 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/search/index.js @@ -0,0 +1,149 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import Overlay from 'react-overlays/lib/Overlay'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; +import DrawerSearchPopout from './popout'; + +// Utils. +import { focusRoot } from 'flavours/glitch/util/dom_helpers'; +import { + assignHandlers, + hiddenComponent, +} from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + placeholder: { + defaultMessage: 'Search', + id: 'search.placeholder', + }, +}); + +// Handlers. +const handlers = { + + blur () { + this.setState({ expanded: false }); + }, + + change ({ target: { value } }) { + const { onChange } = this.props; + if (onChange) { + onChange(value); + } + }, + + clear (e) { + const { + onClear, + submitted, + value: { length }, + } = this.props; + e.preventDefault(); // Prevents focus change ?? + if (onClear && (submitted || length)) { + onClear(); + } + }, + + focus () { + const { onShow } = this.props; + this.setState({ expanded: true }); + if (onShow) { + onShow(); + } + }, + + keyUp (e) { + const { onSubmit } = this.props; + switch (e.key) { + case 'Enter': + if (onSubmit) { + onSubmit(); + } + break; + case 'Escape': + focusRoot(); + } + }, +}; + +// The component. +export default class DrawerSearch extends React.PureComponent { + + constructor (props) { + super(props); + assignHandlers(this, handlers); + this.state = { expanded: false }; + } + + render () { + const { + blur, + change, + clear, + focus, + keyUp, + } = this.handlers; + const { + intl, + submitted, + value, + } = this.props; + const { expanded } = this.state; + const computedClass = classNames('drawer--search', { active: value.length || submitted }); + + return ( +
+ +
+ + +
+ + +
+ ); + } + +} + +DrawerSearch.propTypes = { + value: PropTypes.string, + submitted: PropTypes.bool, + onChange: PropTypes.func, + onSubmit: PropTypes.func, + onClear: PropTypes.func, + onShow: PropTypes.func, + intl: PropTypes.object, +}; diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js new file mode 100644 index 000000000..bd36275f5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js @@ -0,0 +1,95 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import spring from 'react-motion/lib/spring'; + +// Utils. +import Motion from 'flavours/glitch/util/optional_motion'; + +// Messages. +const messages = defineMessages({ + format: { + defaultMessage: 'Advanced search format', + id: 'search_popout.search_format', + }, + hashtag: { + defaultMessage: 'hashtag', + id: 'search_popout.tips.hashtag', + }, + status: { + defaultMessage: 'status', + id: 'search_popout.tips.status', + }, + text: { + defaultMessage: 'Simple text returns matching display names, usernames and hashtags', + id: 'search_popout.tips.text', + }, + user: { + defaultMessage: 'user', + id: 'search_popout.tips.user', + }, +}); + +const motionSpring = spring(1, { damping: 35, stiffness: 400 }); + +export default function DrawerSearchPopout ({ style }) { + return ( + + {({ opacity, scaleX, scaleY }) => ( +
+

+
    +
  • + #example + {' '} + +
  • +
  • + @username@domain + {' '} + +
  • +
  • + URL + {' '} + +
  • +
  • + URL + {' '} + +
  • +
+ +
+ )} +
+ ); +} + +// Props. +DrawerSearchPopout.propTypes = { style: PropTypes.object }; diff --git a/app/javascript/flavours/glitch/util/dom_helpers.js b/app/javascript/flavours/glitch/util/dom_helpers.js index ee95ef8dd..3e1f4a26d 100644 --- a/app/javascript/flavours/glitch/util/dom_helpers.js +++ b/app/javascript/flavours/glitch/util/dom_helpers.js @@ -4,3 +4,11 @@ import detectPassiveEvents from 'detect-passive-events'; // This will either be a passive lister options object (if passive // events are supported), or `false`. export const withPassive = detectPassiveEvents.hasSupport ? { passive: true } : false; + +// Focuses the root element. +export function focusRoot () { + let e; + if (document && (e = document.querySelector('.ui')) && (e = e.parentElement)) { + e.focus(); + } +} diff --git a/app/javascript/flavours/glitch/util/react_helpers.js b/app/javascript/flavours/glitch/util/react_helpers.js index 0826f3584..087e3969d 100644 --- a/app/javascript/flavours/glitch/util/react_helpers.js +++ b/app/javascript/flavours/glitch/util/react_helpers.js @@ -14,7 +14,7 @@ export function assignHandlers (target, handlers) { // This function only returns the component if the result of calling // `test` with `data` is `true`. Useful with funciton binding. export function conditionalRender (test, data, component) { - return test ? component : null; + return test(data) ? component : null; } // This object provides props to make the component not visible. diff --git a/app/javascript/flavours/glitch/util/redux_helpers.js b/app/javascript/flavours/glitch/util/redux_helpers.js index 3bc8bc86f..c0f5eeb28 100644 --- a/app/javascript/flavours/glitch/util/redux_helpers.js +++ b/app/javascript/flavours/glitch/util/redux_helpers.js @@ -1,3 +1,6 @@ +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; + // Merges react-redux props. export function mergeProps (stateProps, dispatchProps, ownProps) { Object.assign({}, ownProps, { @@ -5,3 +8,9 @@ export function mergeProps (stateProps, dispatchProps, ownProps) { state: Object.assign({}, stateProps, ownProps.state || {}), }); } + +// Connects a component. +export function wrap (Component, mapStateToProps, mapDispatchToProps, options) { + const withIntl = typeof options === 'object' ? options.withIntl : !!options; + return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component)); +} -- cgit From b4a3792201ccc01713b536e50428e027bd094d2b Mon Sep 17 00:00:00 2001 From: kibigo! Date: Fri, 29 Dec 2017 16:32:13 -0800 Subject: WIP Refactor; ; dropdowns --- .../flavours/glitch/components/dropdown_menu.js | 11 +- app/javascript/flavours/glitch/components/link.js | 97 ++++ .../features/composer/options/dropdown/index.js | 7 +- .../composer/options/dropdown/item/index.js | 5 +- .../glitch/features/ui/components/actions_modal.js | 75 ++- .../glitch/styles/components/composer.scss | 567 +++++++++++---------- .../flavours/glitch/styles/components/index.scss | 169 +----- 7 files changed, 493 insertions(+), 438 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/link.js (limited to 'app/javascript/flavours/glitch/features/composer/options/dropdown/index.js') diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index d4a886a8b..519941dd6 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -133,8 +133,12 @@ export default class Dropdown extends React.PureComponent { this.props.onModalOpen({ status, - actions: items, - onClick: this.handleItemClick, + actions: items.map( + (item, i) => ({ + ...item, + name: `${item.text}-${i}`, + onClick: this.handleItemClick.bind(i), + }), }); return; @@ -162,8 +166,7 @@ export default class Dropdown extends React.PureComponent { } } - handleItemClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); + handleItemClick = (i, e) => { const { action, to } = this.props.items[i]; this.handleClose(); diff --git a/app/javascript/flavours/glitch/components/link.js b/app/javascript/flavours/glitch/components/link.js new file mode 100644 index 000000000..c49fc487c --- /dev/null +++ b/app/javascript/flavours/glitch/components/link.js @@ -0,0 +1,97 @@ +// Inspired by from Mastodon GO! +// ~ 😘 kibi! + +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +// Utils. +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; + +// Handlers. +const handlers = { + + // We don't handle clicks that are made with modifiers, since these + // often have special browser meanings (eg, "open in new tab"). + click (e) { + const { onClick } = this.props; + if (!onClick || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { + return; + } + onClick(e); + e.preventDefault(); // Prevents following of the link + }, +}; + +// The component. +export default class Link extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + } + + // Rendering. + render () { + const { click } = this.handlers; + const { + children, + className, + href, + onClick, + role, + title, + ...rest + } = this.props; + const computedClass = classNames('link', className, role); + + // We assume that our `onClick` is a routing function and give it + // the qualities of a link even if no `href` is provided. However, + // if we have neither an `onClick` or an `href`, our link is + // purely presentational. + const conditionalProps = {}; + if (href) { + conditionalProps.href = href; + conditionalProps.onClick = click; + } else if (onClick) { + conditionalProps.onClick = click; + conditionalProps.role = 'link'; + conditionalProps.tabIndex = 0; + } else { + conditionalProps.role = 'presentation'; + } + + // If we were provided a `role` it overwrites any that we may have + // set above. This can be used for "links" which are actually + // buttons. + if (role) { + conditionalProps.role = role; + } + + // Rendering. We set `rel='noopener'` for user privacy, and our + // `target` as `'_blank'`. + return ( + {children} + ); + } + +} + +// Props. +Link.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + href: PropTypes.string, // The link destination + onClick: PropTypes.func, // A function to call instead of opening the link + role: PropTypes.string, // An ARIA role for the link + title: PropTypes.string, // A title for the link +}; diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js index ee52008a7..daed4ec8a 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -80,11 +80,16 @@ const handlers = { }) => ({ ...rest, active: value && name === value, + name, onClick (e) { e.preventDefault(); // Prevents focus from changing onModalClose(); onChange(name); }, + onPassiveClick (e) { + e.preventDefault(); // Prevents focus from changing + onChange(name); + }, }) ), }); @@ -191,7 +196,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { > {({ opacity, scaleX, scaleY }) => (
@@ -100,11 +101,11 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { } }()} {meta ? ( -
+
{text} {meta}
- ) :
{text}
} + ) :
{text}
}
); } diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 0873c282f..020cc0dd6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -6,15 +6,26 @@ import StatusContent from 'flavours/glitch/components/status_content'; import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; -import IconButton from 'flavours/glitch/components/icon_button'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; +import Link from 'flavours/glitch/components/link'; +import Toggle from 'react-toggle'; export default class ActionsModal extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, - actions: PropTypes.array, - onClick: PropTypes.func, + actions: PropTypes.arrayOf(PropTypes.shape({ + active: PropTypes.bool, + href: PropTypes.string, + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string, + on: PropTypes.bool, + onClick: PropTypes.func, + onPassiveClick: PropTypes.func, + text: PropTypes.node, + })), }; renderAction = (action, i) => { @@ -22,17 +33,57 @@ export default class ActionsModal extends ImmutablePureComponent { return
  • ; } - const { icon = null, text, meta = null, active = false, href = '#' } = action; + const { + active, + href, + icon, + meta, + name, + on, + onClick, + onPassiveClick, + text, + } = action; return ( -
  • - - {icon && } -
    -
    {text}
    -
    {meta}
    -
    -
    +
  • + + {function () { + + // We render a `` if we were provided an `on` + // property, and otherwise show an `` if available. + switch (true) { + case on !== null && typeof on !== 'undefined': + return ( + + ); + case !!icon: + return ( + + ); + default: + return null; + } + }()} + {meta ? ( +
    + {text} + {meta} +
    + ) :
    {text}
    } +
  • ); } diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index a8d688ea5..ae9114644 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -1,315 +1,370 @@ -.composer { +.composer { padding: 10px } + +.composer--spoiler { + display: block; + box-sizing: border-box; + margin: 0; + border: none; + border-radius: 4px; padding: 10px; + width: 100%; + outline: 0; + color: $ui-base-color; + background: $simple-background-color; + font-size: 14px; + font-family: inherit; + resize: vertical; + + &:focus { outline: 0 } + @include single-column('screen and (max-width: 630px)') { font-size: 16px } +} + +.composer--warning { + color: darken($ui-secondary-color, 65%); + margin-bottom: 15px; + background: $ui-primary-color; + box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3); + padding: 8px 10px; + border-radius: 4px; + font-size: 13px; + font-weight: 400; + + a { + color: darken($ui-primary-color, 33%); + font-weight: 500; + text-decoration: underline; + + &:active, + &:focus, + &:hover { text-decoration: none } + } +} + +.composer--reply { + margin: 0 0 -2px; + border-radius: 4px 4px 0 0; + padding: 10px; + background: $ui-primary-color; + + & > header { + margin-bottom: 5px; + overflow: hidden; + + & > .account { + & > .avatar { + float: left; + margin-right: 5px; + } + + & > .display_name { + color: $ui-base-color; + display: block; + padding-right: 25px; + max-width: 100%; + line-height: 24px; + text-decoration: none; + overflow: hidden; + } + } + + & > .cancel { + float: right; + line-height: 24px; + } + } + + & > .content { + position: relative; + margin: 10px 0; + padding: 0 12px; + font-size: 14px; + line-height: 20px; + color: $ui-base-color; + word-wrap: break-word; + font-weight: 400; + overflow: visible; + white-space: pre-wrap; + padding-top: 5px; + } + + .emojione { + width: 20px; + height: 20px; + margin: -5px 0 0; + } + + p { + margin-bottom: 20px; + + &:last-child { margin-bottom: 0 } + } + + a { + color: lighten($ui-base-color, 20%); + text-decoration: none; + + &:hover { text-decoration: underline } + + &.mention { + &:hover { + text-decoration: none; + + span { text-decoration: underline } + } + } + } +} - .composer--spoiler { +.composer--textarea { + background: $simple-background-color; + position: relative; + + &:disabled { background: $ui-secondary-color } + + & > .textarea { display: block; box-sizing: border-box; margin: 0; border: none; - border-radius: 4px; - padding: 10px; + border-radius: 4px 4px 0 0; + padding: 10px 32px 0 10px; width: 100%; + min-height: 100px; outline: 0; color: $ui-base-color; background: $simple-background-color; font-size: 14px; font-family: inherit; - resize: vertical; + resize: none; &:focus { outline: 0 } @include single-column('screen and (max-width: 630px)') { font-size: 16px } + + @include limited-single-column('screen and (max-width: 600px)') { + height: 100px !important; // prevent auto-resize textarea + resize: vertical; + } } +} - .composer--warning { - color: darken($ui-secondary-color, 65%); - margin-bottom: 15px; - background: $ui-primary-color; - box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3); - padding: 8px 10px; - border-radius: 4px; - font-size: 13px; - font-weight: 400; +.composer--textarea--suggestions { + display: block; + position: absolute; + box-sizing: border-box; + top: 100%; + border-radius: 0 0 4px 4px; + padding: 6px; + width: 100%; + color: $ui-base-color; + background: $ui-secondary-color; + box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); + font-size: 14px; + z-index: 99; + + &[hidden] { display: none } +} - a { - color: darken($ui-primary-color, 33%); - font-weight: 500; - text-decoration: underline; +.composer--textarea--suggestions--item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + border-radius: 4px; + padding: 10px; + font-size: 14px; + line-height: 18px; + cursor: pointer; - &:active, - &:focus, - &:hover { text-decoration: none } + &:hover, + &:focus, + &:active, + &.active { background: darken($ui-secondary-color, 10%) } + + & > .emoji { + img { + display: block; + float: left; + margin-right: 8px; + width: 16px; + height: 16px; } } +} - .composer--reply { - margin: 0 0 -2px; - border-radius: 4px 4px 0 0; - padding: 10px; - background: $ui-primary-color; - - & > header { - margin-bottom: 5px; - overflow: hidden; - - & > .account { - & > .avatar { - float: left; - margin-right: 5px; - } - - & > .display_name { - color: $ui-base-color; - display: block; - padding-right: 25px; - max-width: 100%; - line-height: 24px; - text-decoration: none; - overflow: hidden; - } - } +.composer--upload_form { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 5px; + color: $ui-base-color; + background: $simple-background-color; + font-size: 14px; + font-family: inherit; + overflow: hidden; +} - & > .cancel { - float: right; - line-height: 24px; - } - } +.composer--upload_form--item { + flex: 1 1 0; + margin: 5px; + min-width: 40%; - & > .content { - position: relative; - margin: 10px 0; - padding: 0 12px; + & > div { + position: relative; + border-radius: 4px; + height: 100px; + width: 100%; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + + input { + display: block; + position: absolute; + box-sizing: border-box; + bottom: 0; + left: 0; + margin: 0; + border: 0; + padding: 10px; + width: 100%; + color: $ui-secondary-color; + background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); font-size: 14px; - line-height: 20px; - color: $ui-base-color; - word-wrap: break-word; - font-weight: 400; - overflow: visible; - white-space: pre-wrap; - padding-top: 5px; - } + font-family: inherit; + font-weight: 500; + opacity: 0; + z-index: 2; + transition: opacity .1s ease; + + &:focus { color: $white } - .emojione { - width: 20px; - height: 20px; - margin: -5px 0 0; + &::placeholder { + opacity: 0.54; + color: $ui-secondary-color; + } } - p { - margin-bottom: 20px; + & > .close { mix-blend-mode: difference } + } - &:last-child { margin-bottom: 0 } + &.active { + & > div { + input { opacity: 1 } } + } +} - a { - color: lighten($ui-base-color, 20%); - text-decoration: none; +.composer--options { + padding: 10px; + background: darken($simple-background-color, 8%); + box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05); + border-radius: 0 0 4px 4px; + + & > * { + display: inline-block; + box-sizing: content-box; + padding: 0 3px; + line-height: 27px; + } - &:hover { text-decoration: underline } + & > hr { + display: inline-block; + margin: 0 3px; + border-width: 0 0 0 1px; + border-style: none none none solid; + border-color: transparent transparent transparent darken($simple-background-color, 24%); + padding: 0; + background: transparent; + } +} - &.mention { - &:hover { - text-decoration: none; +.composer--options--dropdown { + & > .value { transition: none } - span { text-decoration: underline } - } - } + &.active { + & > .value { + border-radius: 4px 4px 0 0; + box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); + color: $primary-text-color; + background: $ui-highlight-color; } } +} - .composer--textarea { - background: $simple-background-color; - position: relative; +.composer--options--dropdown__dropdown { + position: absolute; + margin-left: 40px; + border-radius: 4px; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + background: $simple-background-color; + overflow: hidden; + transform-origin: 50% 0; +} - &:disabled { background: $ui-secondary-color } +.composer--options--dropdown--item { + color: $ui-base-color; + padding: 10px; + cursor: pointer; + display: flex; - & > .textarea { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - border-radius: 4px 4px 0 0; - padding: 10px 32px 0 10px; - width: 100%; - min-height: 100px; - outline: 0; - color: $ui-base-color; - background: $simple-background-color; - font-size: 14px; - font-family: inherit; - resize: none; + & > .content { + flex: 1 1 auto; + color: darken($ui-primary-color, 24%); - &:focus { outline: 0 } - @include single-column('screen and (max-width: 630px)') { font-size: 16px } + &:not(:first-child) { margin-left: 10px } - @include limited-single-column('screen and (max-width: 600px)') { - height: 100px !important; // prevent auto-resize textarea - resize: vertical; - } - } - - .composer--textarea--suggestions { + strong { display: block; - position: absolute; - box-sizing: border-box; - top: 100%; - border-radius: 0 0 4px 4px; - padding: 6px; - width: 100%; color: $ui-base-color; - background: $ui-secondary-color; - box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); - font-size: 14px; - z-index: 99; - - &[hidden] { display: none } - - .composer--textarea--suggestions--item { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - border-radius: 4px; - padding: 10px; - font-size: 14px; - line-height: 18px; - cursor: pointer; - - &:hover, - &:focus, - &:active, - &.active { background: darken($ui-secondary-color, 10%) } - - & > .emoji { - img { - display: block; - float: left; - margin-right: 8px; - width: 16px; - height: 16px; - } - } - } + font-weight: 500; } } - .composer--upload_form { - display: flex; - flex-direction: row; - flex-wrap: wrap; - padding: 5px; - color: $ui-base-color; - background: $simple-background-color; - font-size: 14px; - font-family: inherit; - overflow: hidden; + &:hover, + &.active { + background: $ui-highlight-color; + color: $primary-text-color; - .composer--upload_form--item { - flex: 1 1 0; - margin: 5px; - min-width: 40%; - - & > div { - position: relative; - border-radius: 4px; - height: 100px; - width: 100%; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - - input { - display: block; - position: absolute; - box-sizing: border-box; - bottom: 0; - left: 0; - margin: 0; - border: 0; - padding: 10px; - width: 100%; - color: $ui-secondary-color; - background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); - font-size: 14px; - font-family: inherit; - font-weight: 500; - opacity: 0; - z-index: 2; - transition: opacity .1s ease; - - &:focus { color: $white } - - &::placeholder { - opacity: 0.54; - color: $ui-secondary-color; - } - } - - & > .close { mix-blend-mode: difference } - } + & > .content { + color: $primary-text-color; - &.active { - & > div { - input { opacity: 1 } - } - } + strong { color: $primary-text-color } } } - .composer--options { - padding: 10px; - background: darken($simple-background-color, 8%); - box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05); - border-radius: 0 0 4px 4px; - - & > * { - display: inline-block; - box-sizing: content-box; - padding: 0 3px; - line-height: 27px; - } + &.active:hover { background: lighten($ui-highlight-color, 4%) } +} - & > hr { - display: inline-block; - margin: 0 3px; - border-width: 0 0 0 1px; - border-style: none none none solid; - border-color: transparent transparent transparent darken($simple-background-color, 24%); - padding: 0; - background: transparent; - } - } +.composer--publisher { + padding-top: 10px; + text-align: right; + white-space: nowrap; + overflow: hidden; - .composer--publisher { + & > .count { + display: inline-block; + margin: 0 16px 0 8px; padding-top: 10px; - text-align: right; - white-space: nowrap; - overflow: hidden; - - & > .count { - display: inline-block; - margin: 0 16px 0 8px; - padding-top: 10px; - font-size: 16px; - line-height: 36px; - } + font-size: 16px; + line-height: 36px; + } - & > .primary { - display: inline-block; - margin: 0; - padding: 0 10px; - text-align: center; - } + & > .primary { + display: inline-block; + margin: 0; + padding: 0 10px; + text-align: center; + } - & > .side_arm { - display: inline-block; - margin: 0 2px 0 0; - padding: 0; - width: 36px; - text-align: center; - } + & > .side_arm { + display: inline-block; + margin: 0 2px 0 0; + padding: 0; + width: 36px; + text-align: center; + } - &.over { - & > .count { color: $warning-red } - } + &.over { + & > .count { color: $warning-red } } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index e9127affc..98ed5d24a 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -2784,156 +2784,6 @@ filter: none; } -.privacy-dropdown__dropdown { - position: absolute; - background: $simple-background-color; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - border-radius: 4px; - margin-left: 40px; - overflow: hidden; - transform-origin: 50% 0; -} - -.privacy-dropdown__option { - color: $ui-base-color; - padding: 10px; - cursor: pointer; - display: flex; - - &:hover, - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - - .privacy-dropdown__option__content { - color: $primary-text-color; - - strong { - color: $primary-text-color; - } - } - } - - &.active:hover { - background: lighten($ui-highlight-color, 4%); - } -} - -.privacy-dropdown__option__icon { - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; -} - -.privacy-dropdown__option__content { - flex: 1 1 auto; - color: darken($ui-primary-color, 24%); - - strong { - font-weight: 500; - display: block; - color: $ui-base-color; - } -} - -.privacy-dropdown.active { - .privacy-dropdown__value { - background: $simple-background-color; - border-radius: 4px 4px 0 0; - box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); - - .icon-button { - transition: none; - } - - &.active { - background: $ui-highlight-color; - - .icon-button { - color: $primary-text-color; - } - } - } - - .privacy-dropdown__dropdown { - display: block; - box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); - } -} - -.advanced-options-dropdown { - position: relative; -} - -.advanced-options-dropdown__dropdown { - display: none; - position: absolute; - left: 0; - top: 27px; - width: 210px; - background: $simple-background-color; - border-radius: 0 4px 4px; - z-index: 2; - overflow: hidden; -} - -.advanced-options-dropdown__option { - color: $ui-base-color; - padding: 10px; - cursor: pointer; - display: flex; - - &:hover, - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - - .advanced-options-dropdown__option__content { - color: $primary-text-color; - - strong { - color: $primary-text-color; - } - } - } - - &.active:hover { - background: lighten($ui-highlight-color, 4%); - } -} - -.advanced-options-dropdown__option__toggle { - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; -} - -.advanced-options-dropdown__option__content { - flex: 1 1 auto; - color: darken($ui-primary-color, 24%); - - strong { - font-weight: 500; - display: block; - color: $ui-base-color; - } -} - -.advanced-options-dropdown.open { - .advanced-options-dropdown__value { - background: $simple-background-color; - border-radius: 4px 4px 0 0; - box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); - } - - .advanced-options-dropdown__dropdown { - display: block; - box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); - } -} - .modal-root { transition: opacity 0.3s linear; will-change: opacity; @@ -3488,7 +3338,7 @@ max-height: 80vh; max-width: 80vw; - .actions-modal__item-label { + strong { font-weight: 500; } @@ -3501,31 +3351,24 @@ } li:not(:empty) { - a { + & > .link { color: $ui-base-color; display: flex; padding: 12px 16px; font-size: 15px; align-items: center; text-decoration: none; - - &, - button { - transition: none; - } + transition: none; &.active, &:hover, &:active, &:focus { - &, - button { - background: $ui-highlight-color; - color: $primary-text-color; - } + background: $ui-highlight-color; + color: $primary-text-color; } - button:first-child { + & > .icon { margin-right: 10px; } } -- cgit From 42f50049ff29ccdc484c22f8a5a19cda2dd03a5b Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 3 Jan 2018 12:36:21 -0800 Subject: WIP Refactor; 1000 tiny edits --- .../flavours/glitch/components/dropdown_menu.js | 5 +- app/javascript/flavours/glitch/components/link.js | 2 +- .../flavours/glitch/features/composer/index.js | 378 ++++++++++----------- .../composer/options/dropdown/content/index.js | 138 ++++++++ .../options/dropdown/content/item/index.js | 126 +++++++ .../features/composer/options/dropdown/index.js | 221 ++++++------ .../composer/options/dropdown/item/index.js | 127 ------- .../glitch/features/composer/options/index.js | 25 +- .../glitch/features/composer/publisher/index.js | 7 +- .../glitch/features/composer/reply/index.js | 12 +- .../glitch/features/composer/spoiler/index.js | 6 +- .../glitch/features/composer/textarea/index.js | 42 +-- .../composer/textarea/suggestions/index.js | 6 +- .../composer/textarea/suggestions/item/index.js | 6 +- .../glitch/features/composer/upload_form/index.js | 39 ++- .../features/composer/upload_form/item/index.js | 40 +-- .../glitch/features/drawer/account/index.js | 5 +- .../glitch/features/drawer/header/index.js | 3 +- .../flavours/glitch/features/drawer/index.js | 88 ++--- .../glitch/features/drawer/results/index.js | 6 +- .../glitch/features/drawer/search/index.js | 34 +- .../glitch/features/drawer/search/popout/index.js | 4 + .../glitch/features/ui/components/actions_modal.js | 2 +- .../glitch/features/ui/components/columns_area.js | 4 +- .../flavours/glitch/features/ui/index.js | 12 +- app/javascript/flavours/glitch/reducers/compose.js | 5 +- .../glitch/styles/components/composer.scss | 153 ++++++--- .../flavours/glitch/styles/components/drawer.scss | 335 +++++++++--------- .../flavours/glitch/styles/components/index.scss | 43 +-- app/javascript/flavours/glitch/theme.yml | 2 +- .../flavours/glitch/util/async-components.js | 4 +- .../flavours/glitch/util/react_helpers.js | 4 +- .../flavours/glitch/util/redux_helpers.js | 10 +- 33 files changed, 986 insertions(+), 908 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js delete mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js (limited to 'app/javascript/flavours/glitch/features/composer/options/dropdown/index.js') diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 519941dd6..706390c92 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -134,11 +134,12 @@ export default class Dropdown extends React.PureComponent { this.props.onModalOpen({ status, actions: items.map( - (item, i) => ({ + (item, i) => item ? { ...item, name: `${item.text}-${i}`, onClick: this.handleItemClick.bind(i), - }), + } : null + ), }); return; diff --git a/app/javascript/flavours/glitch/components/link.js b/app/javascript/flavours/glitch/components/link.js index c49fc487c..de99f7d42 100644 --- a/app/javascript/flavours/glitch/components/link.js +++ b/app/javascript/flavours/glitch/components/link.js @@ -45,7 +45,7 @@ export default class Link extends React.PureComponent { title, ...rest } = this.props; - const computedClass = classNames('link', className, role); + const computedClass = classNames('link', className, `role-${role}`); // We assume that our `onClick` is a routing function and give it // the qualities of a link even if no `href` is provided. However, diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index c3e6c987c..d64bee7ee 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -52,6 +52,7 @@ function mapStateToProps (state) { focusDate: state.getIn(['compose', 'focusDate']), isSubmitting: state.getIn(['compose', 'is_submitting']), isUploading: state.getIn(['compose', 'is_uploading']), + layout: state.getIn(['local_settings', 'layout']), media: state.getIn(['compose', 'media_attachments']), preselectDate: state.getIn(['compose', 'preselectDate']), privacy: state.getIn(['compose', 'privacy']), @@ -71,132 +72,96 @@ function mapStateToProps (state) { }; // Dispatch mapping. -const mapDispatchToProps = dispatch => ({ - cancelReply () { - dispatch(cancelReplyCompose()); - }, - changeDescription (mediaId, description) { - dispatch(changeUploadCompose(mediaId, description)); - }, - changeSensitivity () { - dispatch(changeComposeSensitivity()); - }, - changeSpoilerText (checked) { - dispatch(changeComposeSpoilerText(checked)); - }, - changeSpoilerness () { - dispatch(changeComposeSpoilerness()); - }, - changeText (text) { - dispatch(changeCompose(text)); - }, - changeVisibility (value) { - dispatch(changeComposeVisibility(value)); - }, - clearSuggestions () { - dispatch(clearComposeSuggestions()); - }, - closeModal () { - dispatch(closeModal()); - }, - fetchSuggestions (token) { - dispatch(fetchComposeSuggestions(token)); - }, - insertEmoji (position, data) { - dispatch(insertEmojiCompose(position, data)); - }, - openActionsModal (data) { - dispatch(openModal('ACTIONS', data)); - }, - openDoodleModal () { - dispatch(openModal('DOODLE', { noEsc: true })); - }, - selectSuggestion (position, token, accountId) { - dispatch(selectComposeSuggestion(position, token, accountId)); - }, - submit () { - dispatch(submitCompose()); - }, - toggleAdvancedOption (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - undoUpload (mediaId) { - dispatch(undoUploadCompose(mediaId)); - }, - upload (files) { - dispatch(uploadCompose(files)); - }, -}); +const mapDispatchToProps = { + onCancelReply: cancelReplyCompose, + onChangeDescription: changeUploadCompose, + onChangeSensitivity: changeComposeSensitivity, + onChangeSpoilerText: changeComposeSpoilerText, + onChangeSpoilerness: changeComposeSpoilerness, + onChangeText: changeCompose, + onChangeVisibility: changeComposeVisibility, + onClearSuggestions: clearComposeSuggestions, + onCloseModal: closeModal, + onFetchSuggestions: fetchComposeSuggestions, + onInsertEmoji: insertEmojiCompose, + onOpenActionsModal: openModal.bind(null, 'ACTIONS'), + onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }), + onSelectSuggestion: selectComposeSuggestion, + onSubmit: submitCompose, + onToggleAdvancedOption: toggleComposeAdvancedOption, + onUndoUpload: undoUploadCompose, + onUpload: uploadCompose, +}; // Handlers. const handlers = { // Changes the text value of the spoiler. - changeSpoiler ({ target: { value } }) { - const { dispatch: { changeSpoilerText } } = this.props; - if (changeSpoilerText) { - changeSpoilerText(value); + handleChangeSpoiler ({ target: { value } }) { + const { onChangeSpoilerText } = this.props; + if (onChangeSpoilerText) { + onChangeSpoilerText(value); } }, // Inserts an emoji at the caret. - emoji (data) { + handleEmoji (data) { const { textarea: { selectionStart } } = this; - const { dispatch: { insertEmoji } } = this.props; + const { onInsertEmoji } = this.props; this.caretPos = selectionStart + data.native.length + 1; - if (insertEmoji) { - insertEmoji(selectionStart, data); + if (onInsertEmoji) { + onInsertEmoji(selectionStart, data); } }, // Handles the secondary submit button. - secondarySubmit () { - const { submit } = this.handlers; + handleSecondarySubmit () { + const { handleSubmit } = this.handlers; const { - dispatch: { changeVisibility }, - side_arm, + onChangeVisibility, + sideArm, } = this.props; - if (changeVisibility) { - changeVisibility(side_arm); + if (sideArm !== 'none' && onChangeVisibility) { + onChangeVisibility(sideArm); } - submit(); + handleSubmit(); }, // Selects a suggestion from the autofill. - select (tokenStart, token, value) { - const { dispatch: { selectSuggestion } } = this.props; + handleSelect (tokenStart, token, value) { + const { onSelectSuggestion } = this.props; this.caretPos = null; - if (selectSuggestion) { - selectSuggestion(tokenStart, token, value); + if (onSelectSuggestion) { + onSelectSuggestion(tokenStart, token, value); } }, // Submits the status. - submit () { + handleSubmit () { const { textarea: { value } } = this; const { - dispatch: { - changeText, - submit, - }, - state: { text }, + onChangeText, + onSubmit, + text, } = this.props; // If something changes inside the textarea, then we update the // state before submitting. - if (changeText && text !== value) { - changeText(value); + if (onChangeText && text !== value) { + onChangeText(value); } // Submits the status. - if (submit) { - submit(); + if (onSubmit) { + onSubmit(); } }, // Sets a reference to the textarea. - refTextarea ({ textarea }) { - this.textarea = textarea; + handleRefTextarea (textareaComponent) { + if (textareaComponent) { + this.textarea = textareaComponent.textarea; + } }, }; @@ -216,10 +181,10 @@ class Composer extends React.Component { // If this is the update where we've finished uploading, // save the last caret position so we can restore it below! componentWillReceiveProps (nextProps) { - const { textarea: { selectionStart } } = this; - const { state: { isUploading } } = this.props; - if (isUploading && !nextProps.state.isUploading) { - this.caretPos = selectionStart; + const { textarea } = this; + const { isUploading } = this.props; + if (textarea && isUploading && !nextProps.isUploading) { + this.caretPos = textarea.selectionStart; } } @@ -239,20 +204,18 @@ class Composer extends React.Component { textarea, } = this; const { - state: { - focusDate, - isUploading, - isSubmitting, - preselectDate, - text, - }, + focusDate, + isUploading, + isSubmitting, + preselectDate, + text, } = this.props; let selectionEnd, selectionStart; // Caret/selection handling. - if (focusDate !== prevProps.state.focusDate || (prevProps.state.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) { + if (focusDate !== prevProps.focusDate || (prevProps.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) { switch (true) { - case preselectDate !== prevProps.state.preselectDate: + case preselectDate !== prevProps.preselectDate: selectionStart = text.search(/\s/) + 1; selectionEnd = text.length; break; @@ -262,71 +225,71 @@ class Composer extends React.Component { default: selectionStart = selectionEnd = text.length; } - textarea.setSelectionRange(selectionStart, selectionEnd); - textarea.focus(); + if (textarea) { + textarea.setSelectionRange(selectionStart, selectionEnd); + textarea.focus(); + } // Refocuses the textarea after submitting. - } else if (prevProps.state.isSubmitting && !isSubmitting) { + } else if (textarea && prevProps.isSubmitting && !isSubmitting) { textarea.focus(); } } render () { const { - changeSpoiler, - emoji, - secondarySubmit, - select, - submit, - refTextarea, + handleChangeSpoiler, + handleEmoji, + handleSecondarySubmit, + handleSelect, + handleSubmit, + handleRefTextarea, } = this.handlers; const { history } = this.context; const { - dispatch: { - cancelReply, - changeDescription, - changeSensitivity, - changeText, - changeVisibility, - clearSuggestions, - closeModal, - fetchSuggestions, - openActionsModal, - openDoodleModal, - toggleAdvancedOption, - undoUpload, - upload, - }, + acceptContentTypes, + amUnlocked, + doNotFederate, intl, - state: { - acceptContentTypes, - amUnlocked, - doNotFederate, - isSubmitting, - isUploading, - media, - privacy, - progress, - replyAccount, - replyContent, - resetFileKey, - sensitive, - showSearch, - sideArm, - spoiler, - spoilerText, - suggestions, - text, - }, + isSubmitting, + isUploading, + layout, + media, + onCancelReply, + onChangeDescription, + onChangeSensitivity, + onChangeSpoilerness, + onChangeText, + onChangeVisibility, + onClearSuggestions, + onCloseModal, + onFetchSuggestions, + onOpenActionsModal, + onOpenDoodleModal, + onToggleAdvancedOption, + onUndoUpload, + onUpload, + privacy, + progress, + replyAccount, + replyContent, + resetFileKey, + sensitive, + showSearch, + sideArm, + spoiler, + spoilerText, + suggestions, + text, } = this.props; return ( -
    +