this.props.onModalOpen({
status,
actions: items.map(
- (item, i) => ({
+ (item, i) => item ? {
...item,
name: `${item.text}-${i}`,
onClick: this.handleItemClick.bind(i),
- }),
+ } : null
+ ),
});
return;
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,
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']),
};
// 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;
+ }
},
};
// 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;
}
}
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;
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 (
- <div className='compose'>
+ <div className='composer'>
<ComposerSpoiler
hidden={!spoiler}
intl={intl}
- onChange={changeSpoiler}
- onSubmit={submit}
+ onChange={handleChangeSpoiler}
+ onSubmit={handleSubmit}
text={spoilerText}
/>
{privacy === 'private' && amUnlocked ? <ComposerWarning /> : null}
content={replyContent}
history={history}
intl={intl}
- onCancel={cancelReply}
+ onCancel={onCancelReply}
/>
) : null}
<ComposerTextarea
- autoFocus={!showSearch && !isMobile(window.innerWidth)}
+ autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
disabled={isSubmitting}
intl={intl}
- onChange={changeText}
- onPaste={upload}
- onPickEmoji={emoji}
- onSubmit={submit}
- onSuggestionsClearRequested={clearSuggestions}
- onSuggestionsFetchRequested={fetchSuggestions}
- onSuggestionSelected={select}
- ref={refTextarea}
+ onChange={onChangeText}
+ onPaste={onUpload}
+ onPickEmoji={handleEmoji}
+ onSubmit={handleSubmit}
+ onSuggestionsClearRequested={onClearSuggestions}
+ onSuggestionsFetchRequested={onFetchSuggestions}
+ onSuggestionSelected={handleSelect}
+ ref={handleRefTextarea}
suggestions={suggestions}
value={text}
/>
- {media && media.size ? (
+ {isUploading || media && media.size ? (
<ComposerUploadForm
- active={isUploading}
intl={intl}
media={media}
- onChangeDescription={changeDescription}
- onRemove={undoUpload}
+ onChangeDescription={onChangeDescription}
+ onRemove={onUndoUpload}
progress={progress}
+ uploading={isUploading}
/>
) : null}
<ComposerOptions
)}
hasMedia={!!media.size}
intl={intl}
- onChangeSensitivity={changeSensitivity}
- onChangeVisibility={changeVisibility}
- onDoodleOpen={openDoodleModal}
- onModalClose={closeModal}
- onModalOpen={openActionsModal}
- onToggleAdvancedOption={toggleAdvancedOption}
- onUpload={upload}
+ onChangeSensitivity={onChangeSensitivity}
+ onChangeVisibility={onChangeVisibility}
+ onDoodleOpen={onOpenDoodleModal}
+ onModalClose={onCloseModal}
+ onModalOpen={onOpenActionsModal}
+ onToggleAdvancedOption={onToggleAdvancedOption}
+ onToggleSpoiler={onChangeSpoilerness}
+ onUpload={onUpload}
privacy={privacy}
resetFileKey={resetFileKey}
sensitive={sensitive}
/>
<ComposerPublisher
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
- disabled={isSubmitting || isUploading || text.length && text.trim().length === 0}
+ disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
intl={intl}
- onSecondarySubmit={secondarySubmit}
- onSubmit={submit}
+ onSecondarySubmit={handleSecondarySubmit}
+ onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
// Props.
Composer.propTypes = {
- dispatch: PropTypes.objectOf(PropTypes.func).isRequired,
intl: PropTypes.object.isRequired,
- state: PropTypes.shape({
- acceptContentTypes: PropTypes.string,
- amUnlocked: PropTypes.bool,
- doNotFederate: PropTypes.bool,
- focusDate: PropTypes.instanceOf(Date),
- isSubmitting: PropTypes.bool,
- isUploading: PropTypes.bool,
- media: PropTypes.list,
- preselectDate: PropTypes.instanceOf(Date),
- privacy: PropTypes.string,
- progress: PropTypes.number,
- replyAccount: ImmutablePropTypes.map,
- replyContent: PropTypes.string,
- resetFileKey: PropTypes.string,
- sideArm: PropTypes.string,
- sensitive: PropTypes.bool,
- showSearch: PropTypes.bool,
- spoiler: PropTypes.bool,
- spoilerText: PropTypes.string,
- suggestionToken: PropTypes.string,
- suggestions: ImmutablePropTypes.list,
- text: PropTypes.string,
- }).isRequired,
-};
-// Default props.
-Composer.defaultProps = {
- dispatch: {},
- state: {},
+ // State props.
+ acceptContentTypes: PropTypes.string,
+ amUnlocked: PropTypes.bool,
+ doNotFederate: PropTypes.bool,
+ focusDate: PropTypes.instanceOf(Date),
+ isSubmitting: PropTypes.bool,
+ isUploading: PropTypes.bool,
+ layout: PropTypes.string,
+ media: ImmutablePropTypes.list,
+ preselectDate: PropTypes.instanceOf(Date),
+ privacy: PropTypes.string,
+ progress: PropTypes.number,
+ replyAccount: ImmutablePropTypes.map,
+ replyContent: PropTypes.string,
+ resetFileKey: PropTypes.number,
+ sideArm: PropTypes.string,
+ sensitive: PropTypes.bool,
+ showSearch: PropTypes.bool,
+ spoiler: PropTypes.bool,
+ spoilerText: PropTypes.string,
+ suggestionToken: PropTypes.string,
+ suggestions: ImmutablePropTypes.list,
+ text: PropTypes.string,
+
+ // Dispatch props.
+ onCancelReply: PropTypes.func,
+ onChangeDescription: PropTypes.func,
+ onChangeSensitivity: PropTypes.func,
+ onChangeSpoilerText: PropTypes.func,
+ onChangeSpoilerness: PropTypes.func,
+ onChangeText: PropTypes.func,
+ onChangeVisibility: PropTypes.func,
+ onClearSuggestions: PropTypes.func,
+ onCloseModal: PropTypes.func,
+ onFetchSuggestions: PropTypes.func,
+ onInsertEmoji: PropTypes.func,
+ onOpenActionsModal: PropTypes.func,
+ onOpenDoodleModal: PropTypes.func,
+ onSelectSuggestion: PropTypes.func,
+ onSubmit: PropTypes.func,
+ onToggleAdvancedOption: PropTypes.func,
+ onUndoUpload: PropTypes.func,
+ onUpload: PropTypes.func,
};
// Connecting and export.
--- /dev/null
+// Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import spring from 'react-motion/lib/spring';
+
+// Components.
+import ComposerOptionsDropdownContentItem from './item';
+
+// Utils.
+import { withPassive } from 'flavours/glitch/util/dom_helpers';
+import Motion from 'flavours/glitch/util/optional_motion';
+import { assignHandlers } from 'flavours/glitch/util/react_helpers';
+
+// Handlers.
+const handlers = {
+
+ // When the document is clicked elsewhere, we close the dropdown.
+ handleDocumentClick ({ target }) {
+ const { node } = this;
+ const { onClose } = this.props;
+ if (onClose && node && !node.contains(target)) {
+ onClose();
+ }
+ },
+
+ // Stores our node in `this.node`.
+ handleRef (node) {
+ this.node = node;
+ },
+};
+
+// The spring to use with our motion.
+const springMotion = spring(1, {
+ damping: 35,
+ stiffness: 400,
+});
+
+// The component.
+export default class ComposerOptionsDropdownContent extends React.PureComponent {
+
+ // Constructor.
+ constructor (props) {
+ super(props);
+ assignHandlers(this, handlers);
+
+ // Instance variables.
+ this.node = null;
+ }
+
+ // On mounting, we add our listeners.
+ componentDidMount () {
+ const { handleDocumentClick } = this.handlers;
+ document.addEventListener('click', handleDocumentClick, false);
+ document.addEventListener('touchend', handleDocumentClick, withPassive);
+ }
+
+ // On unmounting, we remove our listeners.
+ componentWillUnmount () {
+ const { handleDocumentClick } = this.handlers;
+ document.removeEventListener('click', handleDocumentClick, false);
+ document.removeEventListener('touchend', handleDocumentClick, withPassive);
+ }
+
+ // Rendering.
+ render () {
+ const { handleRef } = this.handlers;
+ const {
+ items,
+ onChange,
+ onClose,
+ style,
+ value,
+ } = this.props;
+
+ // The result.
+ return (
+ <Motion
+ defaultStyle={{
+ opacity: 0,
+ scaleX: 0.85,
+ scaleY: 0.75,
+ }}
+ style={{
+ opacity: springMotion,
+ scaleX: springMotion,
+ scaleY: springMotion,
+ }}
+ >
+ {({ opacity, scaleX, scaleY }) => (
+ <div
+ className='composer--options--dropdown--content'
+ ref={handleRef}
+ style={{
+ ...style,
+ opacity: opacity,
+ transform: `scale(${scaleX}, ${scaleY})`,
+ }}
+ >
+ {items.map(
+ ({
+ name,
+ ...rest
+ }) => (
+ <ComposerOptionsDropdownContentItem
+ active={name === value}
+ key={name}
+ name={name}
+ onChange={onChange}
+ onClose={onClose}
+ options={rest}
+ />
+ )
+ )}
+ </div>
+ )}
+ </Motion>
+ );
+ }
+
+}
+
+// Props.
+ComposerOptionsDropdownContent.propTypes = {
+ 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,
+ onClose: PropTypes.func,
+ style: PropTypes.object,
+ value: PropTypes.string,
+};
+
+// Default props.
+ComposerOptionsDropdownContent.defaultProps = { style: {} };
const handlers = {
// This function activates the dropdown item.
- activate (e) {
+ handleActivate (e) {
const {
name,
onChange,
onChange(name);
}
},
-
};
// The component.
-export default class ComposerOptionsDropdownItem extends React.PureComponent {
+export default class ComposerOptionsDropdownContentItem extends React.PureComponent {
// Constructor.
constructor (props) {
// Rendering.
render () {
- const { activate } = this.handlers;
+ const { handleActivate } = this.handlers;
const {
active,
options: {
text,
},
} = this.props;
- const computedClass = classNames('composer--options--dropdown_item', {
+ const computedClass = classNames('composer--options--dropdown--content--item', {
active,
lengthy: meta,
'toggled-off': !on && on !== null && typeof on !== 'undefined',
return (
<div
className={computedClass}
- onClick={activate}
- onKeyDown={activate}
+ onClick={handleActivate}
+ onKeyDown={handleActivate}
role='button'
tabIndex='0'
>
return (
<Toggle
checked={on}
- onChange={activate}
+ onChange={handleActivate}
/>
);
case !!icon:
};
// Props.
-ComposerOptionsDropdownItem.propTypes = {
+ComposerOptionsDropdownContentItem.propTypes = {
active: PropTypes.bool,
name: PropTypes.string,
onChange: PropTypes.func,
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';
+import ComposerOptionsDropdownContent from './content';
// 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 () {
+ handleClose () {
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 }) {
+ handleKeyDown ({ key }) {
const {
- close,
- toggle,
+ handleClose,
+ handleToggle,
} = this.handlers;
switch (key) {
case 'Enter':
- toggle();
+ handleToggle();
break;
case 'Escape':
- close();
+ handleClose();
break;
}
},
- // Toggles opening and closing the dropdown.
- toggle () {
+ // Creates an action modal object.
+ handleMakeModal () {
+ const component = this;
const {
items,
onChange,
- onModalClose,
onModalOpen,
+ onModalClose,
value,
} = this.props;
+
+ // Required props.
+ if (!(onChange && onModalOpen && onModalClose && items)) {
+ return null;
+ }
+
+ // The object.
+ return {
+ actions: items.map(
+ ({
+ name,
+ ...rest
+ }) => ({
+ ...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);
+ component.setState({ needsModalUpdate: true });
+ },
+ })
+ ),
+ };
+ },
+
+ // Toggles opening and closing the dropdown.
+ handleToggle () {
+ const { handleMakeModal } = this.handlers;
+ const { onModalOpen } = 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,
- name,
- onClick (e) {
- e.preventDefault(); // Prevents focus from changing
- onModalClose();
- onChange(name);
- },
- onPassiveClick (e) {
- e.preventDefault(); // Prevents focus from changing
- onChange(name);
- },
- })
- ),
- });
+ if (isUserTouching()) {
+
+ // This gets the modal to open.
+ const modal = handleMakeModal();
+
+ // If we can, we then open the modal.
+ if (modal && onModalOpen) {
+ onModalOpen(modal);
+ return;
}
+ }
// Otherwise, we just set our state to open.
- } else {
- this.setState({ open: !open });
- }
+ this.setState({ open: !open });
},
- // Stores our node in `this.node`.
- ref (node) {
- this.node = node;
+ // If our modal is open and our props update, we need to also update
+ // the modal.
+ handleUpdate () {
+ const { handleMakeModal } = this.handlers;
+ const { onModalOpen } = this.props;
+ const { needsModalUpdate } = this.state;
+
+ // Gets our modal object.
+ const modal = handleMakeModal();
+
+ // Reopens the modal with the new object.
+ if (needsModalUpdate && modal && onModalOpen) {
+ onModalOpen(modal);
+ }
},
};
constructor (props) {
super(props);
assignHandlers(this, handlers);
- this.state = { open: false };
-
- // Instance variables.
- this.node = null;
+ this.state = {
+ needsModalUpdate: false,
+ open: false,
+ };
}
- // 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);
+ // Updates our modal as necessary.
+ componentDidUpdate (prevProps) {
+ const { handleUpdate } = this.handlers;
+ const { items } = this.props;
+ const { needsModalUpdate } = this.state;
+ if (needsModalUpdate && items.find(
+ (item, i) => item.on !== prevProps.items[i].on
+ )) {
+ handleUpdate();
+ this.setState({ needsModalUpdate: false });
+ }
}
// Rendering.
render () {
const {
- close,
- keyDown,
- ref,
- toggle,
+ handleClose,
+ handleKeyDown,
+ handleToggle,
} = this.handlers;
const {
active,
const { open } = this.state;
const computedClass = classNames('composer--options--dropdown', {
active,
- open: open || active,
+ open,
});
// The result.
return (
<div
className={computedClass}
- onKeyDown={keyDown}
- ref={ref}
+ onKeyDown={handleKeyDown}
>
<IconButton
active={open || active}
className='value'
disabled={disabled}
icon={icon}
- onClick={toggle}
+ onClick={handleToggle}
size={18}
style={{
height: null,
title={title}
/>
<Overlay
+ containerPadding={20}
placement='bottom'
show={open}
target={this}
>
- <Motion
- defaultStyle={{
- opacity: 0,
- scaleX: 0.85,
- scaleY: 0.75,
- }}
- style={{
- opacity: springMotion,
- scaleX: springMotion,
- scaleY: springMotion,
- }}
- >
- {({ opacity, scaleX, scaleY }) => (
- <div
- className='composer--options--dropdown__dropdown'
- ref={this.setRef}
- style={{
- opacity: opacity,
- transform: `scale(${scaleX}, ${scaleY})`,
- }}
- >
- {items.map(
- ({
- name,
- ...rest
- }) => (
- <ComposerOptionsDropdownItem
- active={name === value}
- key={name}
- name={name}
- onChange={onChange}
- onClose={close}
- options={rest}
- />
- )
- )}
- </div>
- )}
- </Motion>
+ <ComposerOptionsDropdownContent
+ items={items}
+ onChange={onChange}
+ onClose={handleClose}
+ value={value}
+ />
</Overlay>
</div>
);
const handlers = {
// Handles file selection.
- changeFiles ({ target: { files } }) {
+ handleChangeFiles ({ target: { files } }) {
const { onUpload } = this.props;
if (files.length && onUpload) {
onUpload(files);
},
// Handles attachment clicks.
- clickAttach (name) {
+ handleClickAttach (name) {
const { fileElement } = this;
const { onDoodleOpen } = this.props;
},
// Handles a ref to the file input.
- refFileElement (fileElement) {
+ handleRefFileElement (fileElement) {
this.fileElement = fileElement;
},
};
// Rendering.
render () {
const {
- changeFiles,
- clickAttach,
- refFileElement,
+ handleChangeFiles,
+ handleClickAttach,
+ handleRefFileElement,
} = this.handlers;
const {
acceptContentTypes,
onModalClose,
onModalOpen,
onToggleAdvancedOption,
+ onToggleSpoiler,
privacy,
resetFileKey,
sensitive,
accept={acceptContentTypes}
disabled={disabled || full}
key={resetFileKey}
- onChange={changeFiles}
- ref={refFileElement}
+ onChange={handleChangeFiles}
+ ref={handleRefFileElement}
type='file'
{...hiddenComponent}
/>
text: <FormattedMessage {...messages.doodle} />,
},
]}
- onChange={clickAttach}
+ onChange={handleClickAttach}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
- title={messages.attach}
+ title={intl.formatMessage(messages.attach)}
/>
<Motion
defaultStyle={{ scale: 0.87 }}
active={spoiler}
ariaControls='glitch.composer.spoiler.input'
label='CW'
+ onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)}
/>
<Dropdown
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
+ onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func,
privacy: PropTypes.string,
- resetFileKey: PropTypes.string,
+ resetFileKey: PropTypes.number,
sensitive: PropTypes.bool,
spoiler: PropTypes.bool,
};
// The result.
return (
<div className={computedClass}>
- <span class='count'>{diff}</span>
+ <span className='count'>{diff}</span>
{sideArm && sideArm !== 'none' ? (
<Button
className='side_arm'
+ disabled={disabled || diff < 0}
+ onClick={onSecondarySubmit}
+ style={{ padding: null }}
text={
<span>
<Icon
</span>
}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
- onClick={onSecondarySubmit}
- disabled={disabled || diff < 0}
/>
) : null}
<Button
const handlers = {
// Handles a click on the "close" button.
- click () {
+ handleClick () {
const { onCancel } = this.props;
if (onCancel) {
onCancel();
},
// Handles a click on the status's account.
- clickAccount () {
+ handleClickAccount () {
const {
account,
history,
// Rendering.
render () {
const {
- click,
- clickAccount,
+ handleClick,
+ handleClickAccount,
} = this.handlers;
const {
account,
<IconButton
className='cancel'
icon='times'
- onClick={click}
+ onClick={handleClick}
title={intl.formatMessage(messages.cancel)}
/>
{account ? (
<a
className='account'
href={account.get('url')}
- onClick={clickAccount}
+ onClick={handleClickAccount}
>
<Avatar
account={account}
const handlers = {
// Handles a keypress.
- keyDown ({
+ handleKeyDown ({
ctrlKey,
keyCode,
metaKey,
// Rendering.
render () {
- const { keyDown } = this.handlers;
+ const { handleKeyDown } = this.handlers;
const {
hidden,
intl,
<input
id='glitch.composer.spoiler.input'
onChange={onChange}
- onKeyDown={keyDown}
+ onKeyDown={handleKeyDown}
placeholder={intl.formatMessage(messages.placeholder)}
type='text'
value={text}
const handlers = {
// When blurring the textarea, suggestions are hidden.
- blur () {
+ handleBlur () {
this.setState({ suggestionsHidden: true });
},
// When the contents of the textarea change, we have to pull up new
// autosuggest suggestions if applicable, and also change the value
// of the textarea in our store.
- change ({
+ handleChange ({
target: {
selectionStart,
value,
},
// Handles a click on an autosuggestion.
- clickSuggestion (index) {
+ handleClickSuggestion (index) {
const { textarea } = this;
const {
onSuggestionSelected,
// Handles a keypress. If the autosuggestions are visible, we need
// to allow keypresses to navigate and sleect them.
- keyDown (e) {
+ handleKeyDown (e) {
const {
disabled,
onSubmit,
// When the escape key is released, we either close the suggestions
// window or focus the UI.
- keyUp ({ key }) {
+ handleKeyUp ({ key }) {
const { suggestionsHidden } = this.state;
if (key === 'Escape') {
if (!suggestionsHidden) {
},
// Handles the pasting of images into the composer.
- paste (e) {
+ handlePaste (e) {
const { onPaste } = this.props;
let d;
if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) {
},
// Saves a reference to the textarea.
- refTextarea (textarea) {
+ handleRefTextarea (textarea) {
this.textarea = textarea;
},
};
// Rendering.
render () {
const {
- blur,
- change,
- clickSuggestion,
- keyDown,
- keyUp,
- paste,
- refTextarea,
+ handleBlur,
+ handleChange,
+ handleClickSuggestion,
+ handleKeyDown,
+ handleKeyUp,
+ handlePaste,
+ handleRefTextarea,
} = this.handlers;
const {
autoFocus,
autoFocus={autoFocus}
className='textarea'
disabled={disabled}
- inputRef={refTextarea}
- onBlur={blur}
- onChange={change}
- onKeyDown={keyDown}
- onKeyUp={keyUp}
- onPaste={paste}
+ inputRef={handleRefTextarea}
+ onBlur={handleBlur}
+ onChange={handleChange}
+ onKeyDown={handleKeyDown}
+ onKeyUp={handleKeyUp}
+ onPaste={handlePaste}
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }}
<EmojiPicker onPickEmoji={onPickEmoji} />
<ComposerTextareaSuggestions
hidden={suggestionsHidden}
- onSuggestionClick={clickSuggestion}
+ onSuggestionClick={handleClickSuggestion}
suggestions={suggestions}
value={selectedSuggestion}
/>
return (
<div
className='composer--textarea--suggestions'
- hidden={hidden || suggestions.isEmpty()}
+ hidden={hidden || !suggestions || suggestions.isEmpty()}
>
- {!hidden ? suggestions.map(
+ {!hidden && suggestions ? suggestions.map(
(suggestion, index) => (
<ComposerTextareaSuggestionsItem
index={index}
hidden: PropTypes.bool,
onSuggestionClick: PropTypes.func,
suggestions: ImmutablePropTypes.list,
- value: PropTypes.string,
+ value: PropTypes.number,
};
const handlers = {
// Handles a click on a suggestion.
- click (e) {
+ handleClick (e) {
const {
index,
onClick,
// Rendering.
render () {
- const { click } = this.handlers;
+ const { handleClick } = this.handlers;
const {
selected,
suggestion,
return (
<div
className={computedClass}
- onMouseDown={click}
+ onMouseDown={handleClick}
role='button'
tabIndex='0'
>
// The component.
export default function ComposerUploadForm ({
- active,
intl,
media,
onChangeDescription,
onRemove,
progress,
+ uploading,
}) {
- const computedClass = classNames('composer--upload_form', { uploading: active });
-
- // We need `media` in order to be able to render.
- if (!media) {
- return null;
- }
+ const computedClass = classNames('composer--upload_form', { uploading });
// The result.
return (
<div className={computedClass}>
- {active ? <ComposerUploadFormProgress progress={progress} /> : null}
- {media.map(item => (
- <ComposerUploadFormItem
- description={item.get('description')}
- key={item.get('id')}
- id={item.get('id')}
- intl={intl}
- preview={item.get('preview_url')}
- onChangeDescription={onChangeDescription}
- onRemove={onRemove}
- />
- ))}
+ {uploading ? <ComposerUploadFormProgress progress={progress} /> : null}
+ {media ? (
+ <div className='content'>
+ {media.map(item => (
+ <ComposerUploadFormItem
+ description={item.get('description')}
+ key={item.get('id')}
+ id={item.get('id')}
+ intl={intl}
+ preview={item.get('preview_url')}
+ onChangeDescription={onChangeDescription}
+ onRemove={onRemove}
+ />
+ ))}
+ </div>
+ ) : null}
</div>
);
}
// Props.
ComposerUploadForm.propTypes = {
- active: PropTypes.bool,
intl: PropTypes.object.isRequired,
media: ImmutablePropTypes.list,
onChangeDescription: PropTypes.func,
onRemove: PropTypes.func,
progress: PropTypes.number,
+ uploading: PropTypes.bool,
};
const handlers = {
// On blur, we save the description for the media item.
- blur () {
+ handleBlur () {
const {
id,
onChangeDescription,
// When the value of our description changes, we store it in the
// temp value `dirtyDescription` in our state.
- change ({ target: { value } }) {
+ handleChange ({ target: { value } }) {
this.setState({ dirtyDescription: value });
},
// Records focus on the media item.
- focus () {
+ handleFocus () {
this.setState({ focused: true });
},
// Records the start of a hover over the media item.
- mouseEnter () {
+ handleMouseEnter () {
this.setState({ hovered: true });
},
// Records the end of a hover over the media item.
- mouseLeave () {
+ handleMouseLeave () {
this.setState({ hovered: false });
},
// Removes the media item.
- remove () {
+ handleRemove () {
const {
id,
onRemove,
// Constructor.
constructor (props) {
super(props);
- assignHandlers(handlers);
+ assignHandlers(this, handlers);
this.state = {
hovered: false,
focused: false,
// Rendering.
render () {
const {
- blur,
- change,
- focus,
- mouseEnter,
- mouseLeave,
- remove,
+ handleBlur,
+ handleChange,
+ handleFocus,
+ handleMouseEnter,
+ handleMouseLeave,
+ handleRemove,
} = this.handlers;
const {
description,
return (
<div
className={computedClass}
- onMouseEnter={mouseEnter}
- onMouseLeave={mouseLeave}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
>
<Motion
defaultStyle={{ scale: 0.8 }}
<IconButton
className='close'
icon='times'
- onClick={remove}
+ onClick={handleRemove}
size={36}
title={intl.formatMessage(messages.undo)}
/>
<span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
<input
maxLength={420}
- onBlur={blur}
- onChange={change}
- onFocus={focus}
+ onBlur={handleBlur}
+ onChange={handleChange}
+ onFocus={handleFocus}
placeholder={intl.formatMessage(messages.description)}
type='text'
value={dirtyDescription || description || ''}
// Props.
ComposerUploadFormItem.propTypes = {
description: PropTypes.string,
- id: PropTypes.number,
+ id: PropTypes.string,
intl: PropTypes.object.isRequired,
onChangeDescription: PropTypes.func,
onRemove: PropTypes.func,
// We need an account to render.
if (!account) {
return (
- <div className='drawer--pager--account'>
+ <div className='drawer--account'>
<a
className='edit'
href='/settings/profile'
// The result.
return (
- <div className='drawer--pager--account'>
+ <div className='drawer--account'>
<Permalink
className='avatar'
href={account.get('url')}
);
}
+// Props.
DrawerAccount.propTypes = { account: ImmutablePropTypes.map };
}) {
// Only renders the component if the column isn't being shown.
- const renderForColumn = conditionalRender.bind(
+ const renderForColumn = conditionalRender.bind(null,
columnId => !columns || !columns.some(
column => column.get('id') === columnId
)
);
}
+// Props.
DrawerHeader.propTypes = {
columns: ImmutablePropTypes.list,
intl: PropTypes.object,
});
// Dispatch mapping.
-const mapDispatchToProps = dispatch => ({
- change (value) {
- dispatch(changeSearch(value));
- },
- clear () {
- dispatch(clearSearch());
- },
- show () {
- dispatch(showSearch());
- },
- submit () {
- dispatch(submitSearch());
- },
- openSettings () {
- dispatch(openModal('SETTINGS', {}));
- },
-});
+const mapDispatchToProps = {
+ onChange: changeSearch,
+ onClear: clearSearch,
+ onShow: showSearch,
+ onSubmit: submitSearch,
+ onOpenSettings: openModal.bind(null, 'SETTINGS', {}),
+};
// The component.
class Drawer extends React.Component {
// Rendering.
render () {
const {
- dispatch: {
- change,
- clear,
- openSettings,
- show,
- submit,
- },
+ account,
+ columns,
intl,
multiColumn,
- state: {
- account,
- columns,
- results,
- searchHidden,
- searchValue,
- submitted,
- },
+ onChange,
+ onClear,
+ onOpenSettings,
+ onShow,
+ onSubmit,
+ results,
+ searchHidden,
+ searchValue,
+ submitted,
} = this.props;
// The result.
<DrawerHeader
columns={columns}
intl={intl}
- onSettingsClick={openSettings}
+ onSettingsClick={onOpenSettings}
/>
) : null}
<DrawerSearch
intl={intl}
- onChange={change}
- onClear={clear}
- onShow={show}
- onSubmit={submit}
+ onChange={onChange}
+ onClear={onClear}
+ onShow={onShow}
+ onSubmit={onSubmit}
submitted={submitted}
value={searchValue}
/>
// Props.
Drawer.propTypes = {
- dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
- state: PropTypes.shape({
- account: ImmutablePropTypes.map,
- columns: ImmutablePropTypes.list,
- results: ImmutablePropTypes.map,
- searchHidden: PropTypes.bool,
- searchValue: PropTypes.string,
- submitted: PropTypes.bool,
- }).isRequired,
-};
-// Default props.
-Drawer.defaultProps = {
- dispatch: {},
- state: {},
+ // State props.
+ account: ImmutablePropTypes.map,
+ columns: ImmutablePropTypes.list,
+ results: ImmutablePropTypes.map,
+ searchHidden: PropTypes.bool,
+ searchValue: PropTypes.string,
+ submitted: PropTypes.bool,
+
+ // Dispatch props.
+ onChange: PropTypes.func,
+ onClear: PropTypes.func,
+ onShow: PropTypes.func,
+ onSubmit: PropTypes.func,
+ onOpenSettings: PropTypes.func,
};
// Connecting and export.
});
// The component.
-export default function DrawerPager ({
+export default function DrawerResults ({
results,
visible,
}) {
const statuses = results ? results.get('statuses') : null;
const hashtags = results ? results.get('hashtags') : null;
+ // This gets the total number of items.
const count = [accounts, statuses, hashtags].reduce(function (size, item) {
if (item && item.size) {
return size + item.size;
);
}
-DrawerPager.propTypes = {
+// Props.
+DrawerResults.propTypes = {
results: ImmutablePropTypes.map,
visible: PropTypes.bool,
};
// Handlers.
const handlers = {
- blur () {
+ handleBlur () {
this.setState({ expanded: false });
},
- change ({ target: { value } }) {
+ handleChange ({ target: { value } }) {
const { onChange } = this.props;
if (onChange) {
onChange(value);
}
},
- clear (e) {
+ handleClear (e) {
const {
onClear,
submitted,
}
},
- focus () {
+ handleFocus () {
const { onShow } = this.props;
this.setState({ expanded: true });
if (onShow) {
}
},
- keyUp (e) {
+ handleKeyUp (e) {
const { onSubmit } = this.props;
switch (e.key) {
case 'Enter':
// The component.
export default class DrawerSearch extends React.PureComponent {
+ // Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
this.state = { expanded: false };
}
+ // Rendering.
render () {
const {
- blur,
- change,
- clear,
- focus,
- keyUp,
+ handleBlur,
+ handleChange,
+ handleClear,
+ handleFocus,
+ handleKeyUp,
} = this.handlers;
const {
intl,
type='text'
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
- onChange={change}
- onKeyUp={keyUp}
- onFocus={focus}
- onBlur={blur}
+ onChange={handleChange}
+ onKeyUp={handleKeyUp}
+ onFocus={handleFocus}
+ onBlur={handleBlur}
/>
</label>
<div
aria-label={intl.formatMessage(messages.placeholder)}
className='icon'
- onClick={clear}
+ onClick={handleClear}
role='button'
tabIndex='0'
>
<Icon icon='search' />
<Icon icon='fa-times-circle' />
</div>
-
<Overlay
placement='bottom'
show={expanded && !(value || '').length && !submitted}
}
+// Props.
DrawerSearch.propTypes = {
value: PropTypes.string,
submitted: PropTypes.bool,
},
});
+// The spring used by our motion.
const motionSpring = spring(1, { damping: 35, stiffness: 400 });
+// The component.
export default function DrawerSearchPopout ({ style }) {
+
+ // The result.
return (
<Motion
defaultStyle={{
<Link
className={classNames('link', { active })}
href={href}
- onClick={onClick}
+ onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
role={onClick ? 'button' : null}
>
{function () {
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
+import { Drawer, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from 'flavours/glitch/util/scroll';
const componentMap = {
- 'COMPOSE': Compose,
+ 'COMPOSE': Drawer,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
import ColumnsAreaContainer from './containers/columns_area_container';
import classNames from 'classnames';
import {
- Compose,
+ Drawer,
Status,
GettingStarted,
KeyboardShortcuts,
});
const mapStateToProps = state => ({
- isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']) !== '',
layout: state.getIn(['local_settings', 'layout']),
isWide: state.getIn(['local_settings', 'stretch']),
};
handleBeforeUnload = (e) => {
- const { intl, isComposing, hasComposingText } = this.props;
+ const { intl, hasComposingText } = this.props;
- if (isComposing && hasComposingText) {
+ if (hasComposingText) {
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,
// but we set user-friendly message for other browsers, e.g. Edge.
}
shouldComponentUpdate (nextProps) {
- if (nextProps.isComposing !== this.props.isComposing) {
+ if (nextProps.navbarUnder !== this.props.navbarUnder) {
// Avoid expensive update just to toggle a class
- this.node.classList.toggle('is-composing', nextProps.isComposing);
this.node.classList.toggle('navbar-under', nextProps.navbarUnder);
return false;
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
- <WrappedRoute path='/statuses/new' component={Compose} content={children} />
+ <WrappedRoute path='/statuses/new' component={Drawer} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
focusDate: null,
preselectDate: null,
in_reply_to: null,
- is_composing: false,
is_submitting: false,
is_uploading: false,
progress: 0,
case COMPOSE_MOUNT:
return state.set('mounted', true);
case COMPOSE_UNMOUNT:
- return state
- .set('mounted', false)
- .set('is_composing', false);
+ return state.set('mounted', false)
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
return state
.set('advanced_options',
.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;
+ input {
+ 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 }
+ &:focus { outline: 0 }
+ @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+ }
}
.composer--warning {
}
.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 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;
+ & > label {
+ .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;
- &:focus { outline: 0 }
- @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+ &:disabled { background: $ui-secondary-color }
+ &: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;
+ @include limited-single-column('screen and (max-width: 600px)') {
+ height: 100px !important; // prevent auto-resize textarea
+ resize: vertical;
+ }
}
}
}
}
.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;
+
+ & > .content {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ font-family: inherit;
+ overflow: hidden;
+ }
}
.composer--upload_form--item {
}
}
+.composer--upload_form--progress {
+ display: flex;
+ padding: 10px;
+ color: $ui-base-lighter-color;
+ overflow: hidden;
+
+ & > .fa {
+ font-size: 34px;
+ margin-right: 10px;
+ }
+
+ & > .message {
+ flex: 1 1 auto;
+
+ & > span {
+ display: block;
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+ }
+
+ & > .backdrop {
+ position: relative;
+ margin-top: 5px;
+ border-radius: 6px;
+ width: 100%;
+ height: 6px;
+ background: $ui-base-lighter-color;
+
+ & > .tracker {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 6px;
+ border-radius: 6px;
+ background: $ui-highlight-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;
+ height: 27px;
& > * {
display: inline-block;
box-sizing: content-box;
padding: 0 3px;
+ height: 27px;
line-height: 27px;
+ vertical-align: bottom;
}
& > hr {
border-style: none none none solid;
border-color: transparent transparent transparent darken($simple-background-color, 24%);
padding: 0;
+ width: 0;
+ height: 27px;
background: transparent;
}
}
.composer--options--dropdown {
- & > .value { transition: none }
-
- &.active {
+ &.open {
& > .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;
+ transition: none;
}
}
}
-.composer--options--dropdown__dropdown {
+.composer--options--dropdown--content {
position: absolute;
- margin-left: 40px;
border-radius: 4px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
background: $simple-background-color;
transform-origin: 50% 0;
}
-.composer--options--dropdown--item {
- color: $ui-base-color;
+.composer--options--dropdown--content--item {
+ display: flex;
+ align-items: center;
padding: 10px;
+ color: $ui-base-color;
cursor: pointer;
- display: flex;
& > .content {
flex: 1 1 auto;
& > .count {
display: inline-block;
margin: 0 16px 0 8px;
- padding-top: 10px;
font-size: 16px;
line-height: 36px;
}
box-sizing: border-box;
padding: 10px 5px;
width: 300px;
- flex: 1 1 100%;
+ flex: none;
contain: strict;
&:first-child {
padding-right: 10px;
}
- @include multi-columns('screen and (max-width: 630px)') {
- &, &:first-child, &:last-child {
- padding: 0;
- }
+ @include single-column('screen and (max-width: 630px)') { flex: auto }
+
+ @include limited-single-column('screen and (max-width: 630px)') {
+ &, &:first-child, &:last-child { padding: 0 }
}
.wide & {
flex: 1 1 200px;
}
- .react-swipeable-view-container & {
+ @include single-column('screen and (max-width: 630px)') {
+ :root & { // Overrides `.wide` for single-column view
+ flex: auto;
+ width: 100%;
+ min-width: 0;
+ max-width: none;
+ padding: 0;
+ }
+ }
+
+ .react-swipeable-view-container & { height: 100% }
+
+ & > .contents {
+ position: relative;
+ padding: 0;
+ width: 100%;
height: 100%;
+ background: lighten($ui-base-color, 13%);
+ overflow-x: hidden;
+ overflow-y: auto;
+ contain: strict;
+ }
+}
+
+.drawer--header {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 10px;
+ flex: none;
+ background: lighten($ui-base-color, 8%);
+ font-size: 16px;
+
+ & > * {
+ display: block;
+ box-sizing: border-box;
+ border-bottom: 2px solid transparent;
+ padding: 15px 5px 13px;
+ height: 48px;
+ flex: 1 1 auto;
+ color: $ui-primary-color;
+ text-align: center;
+ text-decoration: none;
+ cursor: pointer;
}
- .drawer--header {
- display: flex;
- flex-direction: row;
- margin-bottom: 10px;
- flex: none;
- background: lighten($ui-base-color, 8%);
- font-size: 16px;
+ a {
+ transition: background 100ms ease-in;
- & > * {
- display: block;
- box-sizing: border-box;
- border-bottom: 2px solid transparent;
- padding: 15px 5px 13px;
- height: 48px;
- flex: 1 1 auto;
- color: $ui-primary-color;
- text-align: center;
- text-decoration: none;
- cursor: pointer;
+ &:focus,
+ &:hover {
+ outline: none;
+ background: lighten($ui-base-color, 3%);
+ transition: background 200ms ease-out;
}
+ }
+}
- a {
- transition: background 100ms ease-in;
+.drawer--search {
+ position: relative;
+ margin-bottom: 10px;
+ flex: none;
- &:focus,
- &:hover {
- outline: none;
- background: lighten($ui-base-color, 3%);
- transition: background 200ms ease-out;
- }
+ @include limited-single-column('screen and (max-width: 360px)') { margin-bottom: 0 }
+ @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+
+ input {
+ display: block;
+ box-sizing: border-box;
+ margin: 0;
+ border: none;
+ padding: 10px 30px 10px 10px;
+ width: 100%;
+ height: 36px;
+ outline: 0;
+ color: $ui-primary-color;
+ background: $ui-base-color;
+ font-size: 14px;
+ font-family: inherit;
+ line-height: 16px;
+
+ &:focus {
+ outline: 0;
+ background: lighten($ui-base-color, 4%);
}
}
- .drawer--search {
- position: relative;
- margin-bottom: 10px;
- flex: none;
+ & > .icon {
+ .fa {
+ display: inline-block;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 18px;
+ height: 18px;
+ color: $ui-secondary-color;
+ font-size: 18px;
+ opacity: 0;
+ cursor: default;
+ pointer-events: none;
+ z-index: 2;
+ transition: all 100ms linear;
+ }
- @include limited-single-column('screen and (max-width: 360px)') {
- margin-bottom: 0;
+ .fa-search {
+ opacity: 0.3;
+ transform: rotate(0deg);
}
- input {
- display: block;
- box-sizing: border-box;
- margin: 0;
- border: none;
- padding: 10px 30px 10px 10px;
- width: 100%;
- height: 36px;
- outline: 0;
- color: $ui-primary-color;
- background: $ui-base-color;
- font-size: 14px;
- font-family: inherit;
- line-height: 16px;
+ .fa-times-circle {
+ top: 11px;
+ transform: rotate(-90deg);
+ cursor: pointer;
- &:focus {
- outline: 0;
- background: lighten($ui-base-color, 4%);
- }
+ &:hover { color: $primary-text-color }
}
- & > .icon {
- .fa {
- display: inline-block;
- position: absolute;
- top: 10px;
- right: 10px;
- width: 18px;
- height: 18px;
- color: $ui-secondary-color;
- font-size: 18px;
+ &.active {
+ .fa-search {
opacity: 0;
- cursor: default;
- pointer-events: none;
- z-index: 2;
- transition: all 100ms linear;
+ transform: rotate(90deg);
}
- .fa-search {
+ .fa-times-circle {
opacity: 0.3;
+ pointer-events: auto;
transform: rotate(0deg);
}
-
- .fa-times-circle {
- top: 11px;
- transform: rotate(-90deg);
- cursor: pointer;
-
- &:hover {
- color: $primary-text-color;
- }
- }
-
- &.active {
- .fa-search {
- opacity: 0;
- transform: rotate(90deg);
- }
-
- .fa-times-circle {
- opacity: 0.3;
- pointer-events: auto;
- transform: rotate(0deg);
- }
- }
}
}
+}
- & > .contents {
- position: relative;
- padding: 0;
- width: 100%;
- height: 100%;
- background: lighten($ui-base-color, 13%);
- overflow-x: hidden;
- overflow-y: auto;
- contain: strict;
-
- .drawer--account {
- padding: 10px;
- color: $ui-primary-color;
+.drawer--account {
+ padding: 10px;
+ color: $ui-primary-color;
- & > a {
- color: inherit;
- text-decoration: none;
- }
+ & > a {
+ color: inherit;
+ text-decoration: none;
+ }
- & > .avatar {
- float: left;
- margin-right: 10px;
- }
+ & > .avatar {
+ float: left;
+ margin-right: 10px;
+ }
- & > .acct {
- display: block;
- color: $primary-text-color;
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
+ & > .acct {
+ display: block;
+ color: $primary-text-color;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
- .drawer--results {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- padding: 0;
- background: $ui-base-color;
- overflow-x: hidden;
- overflow-y: auto;
- contain: strict;
-
- & > header {
- border-bottom: 1px solid darken($ui-base-color, 4%);
- padding: 15px 10px;
- color: $ui-base-lighter-color;
- background: lighten($ui-base-color, 2%);
- font-size: 14px;
- font-weight: 500;
- }
+.drawer--results {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 0;
+ background: $ui-base-color;
+ overflow-x: hidden;
+ overflow-y: auto;
+ contain: strict;
- & > section {
- background: $ui-base-color;
-
- & > .hashtag {
- display: block;
- padding: 10px;
- color: $ui-secondary-color;
- text-decoration: none;
-
- &:hover,
- &:active,
- &:focus {
- color: lighten($ui-secondary-color, 4%);
- text-decoration: underline;
- }
- }
- }
- }
+ & > header {
+ border-bottom: 1px solid darken($ui-base-color, 4%);
+ padding: 15px 10px;
+ color: $ui-base-lighter-color;
+ background: lighten($ui-base-color, 2%);
+ font-size: 14px;
+ font-weight: 500;
}
-}
-:root { // Overrides .wide stylings for mobile view
- @include single-column('screen and (max-width: 630px)', $parent: null) {
- .drawer {
- flex: auto;
- width: 100%;
- min-width: 0;
- max-width: none;
- padding: 0;
+ & > section {
+ background: $ui-base-color;
- .drawer--search input {
- font-size: 16px;
+ & > .hashtag {
+ display: block;
+ padding: 10px;
+ color: $ui-secondary-color;
+ text-decoration: none;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: lighten($ui-secondary-color, 4%);
+ text-decoration: underline;
}
}
}
border-radius: 4px;
}
-.upload-progress {
- padding: 10px;
- color: $ui-base-lighter-color;
- overflow: hidden;
- display: flex;
-
- .fa {
- font-size: 34px;
- margin-right: 10px;
- }
-
- span {
- font-size: 12px;
- text-transform: uppercase;
- font-weight: 500;
- display: block;
- }
-}
-
-.upload-progess__message {
- flex: 1 1 auto;
-}
-
-.upload-progress__backdrop {
- width: 100%;
- height: 6px;
- border-radius: 6px;
- background: $ui-base-lighter-color;
- position: relative;
- margin-top: 5px;
-}
-
-.upload-progress__tracker {
- position: absolute;
- left: 0;
- top: 0;
- height: 6px;
- background: $ui-highlight-color;
- border-radius: 6px;
-}
-
.emoji-button {
display: block;
font-size: 24px;
max-width: 80vw;
strong {
+ display: block;
font-weight: 500;
}
color: $primary-text-color;
}
+ & > .react-toggle,
& > .icon {
margin-right: 10px;
}
home:
filename: packs/home.js
preload:
+ - flavours/glitch/async/drawer
- flavours/glitch/async/getting_started
- - flavours/glitch/async/compose
- flavours/glitch/async/home_timeline
- flavours/glitch/async/notifications
modal:
return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/util/emoji/emoji_picker');
}
-export function Compose () {
- return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose');
+export function Drawer () {
+ return import(/* webpackChunkName: "flavours/glitch/async/drawer" */'flavours/glitch/features/drawer');
}
export function Notifications () {
// We just bind each handler to the `target`.
const handle = target.handlers = {};
- handlers.keys().forEach(
- key => handle.key = key.bind(target)
+ Object.keys(handlers).forEach(
+ key => handle[key] = handlers[key].bind(target)
);
}
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-// Merges react-redux props.
-export function mergeProps (stateProps, dispatchProps, ownProps) {
- Object.assign({}, ownProps, {
- dispatch: Object.assign({}, dispatchProps, ownProps.dispatch || {}),
- 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));
+ return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps)(Component));
}