Skip to content

Commit

Permalink
Change design of compose form in web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron committed Dec 2, 2023
1 parent 10b879b commit a36a1e4
Show file tree
Hide file tree
Showing 22 changed files with 425 additions and 538 deletions.
25 changes: 25 additions & 0 deletions app/javascript/images/warning-stripes.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 25 additions & 32 deletions app/javascript/mastodon/components/autosuggest_textarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const AutosuggestTextarea = forwardRef(({
onFocus,
autoFocus = true,
lang,
children,
}, textareaRef) => {

const [suggestionsHidden, setSuggestionsHidden] = useState(true);
Expand Down Expand Up @@ -183,40 +182,35 @@ const AutosuggestTextarea = forwardRef(({
);
};

return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>

<Textarea
ref={textareaRef}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onKeyUp={onKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
dir='auto'
aria-autocomplete='list'
lang={lang}
/>
</label>
</div>
{children}
</div>,
return (
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>

<Textarea
ref={textareaRef}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onKeyUp={onKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
dir='auto'
aria-autocomplete='list'
lang={lang}
/>
</label>

<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(renderSuggestion)}
</div>
</div>,
];
</div>
);
});

AutosuggestTextarea.propTypes = {
Expand All @@ -232,7 +226,6 @@ AutosuggestTextarea.propTypes = {
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
onFocus:PropTypes.func,
children: PropTypes.node,
autoFocus: PropTypes.bool,
lang: PropTypes.string,
};
Expand Down
145 changes: 69 additions & 76 deletions app/javascript/mastodon/features/compose/components/compose_form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { length } from 'stringz';

import { Icon } from 'mastodon/components/icon';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';

import AutosuggestInput from '../../../components/autosuggest_input';
Expand All @@ -35,10 +33,9 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20

const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning (optional)' },
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save' },
});

class ComposeForm extends ImmutablePureComponent {
Expand Down Expand Up @@ -227,91 +224,87 @@ class ComposeForm extends ImmutablePureComponent {
const { highlighted } = this.state;
const disabled = this.props.isSubmitting;

let publishText = '';

if (this.props.isEditing) {
publishText = intl.formatMessage(messages.saveChanges);
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <><Icon id='lock' icon={LockIcon} /> {intl.formatMessage(messages.publish)}</>;
} else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}

return (
<form className='compose-form' onSubmit={this.handleSubmit}>
<WarningContainer />

<ReplyIndicatorContainer />

<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>
</div>
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
<div>
{this.props.spoiler && (
<div className='spoiler-input'>
<div className='spoiler-input__border' />

<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
disabled={disabled}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>

<div className='spoiler-input__border' />
</div>
)}

<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={autoFocus}
lang={this.props.lang}
/>
</div>

<div className={classNames('compose-form__highlightable', { active: highlighted })}>
<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={autoFocus}
lang={this.props.lang}
>
<div className='compose-form__modifiers'>
<UploadFormContainer />
<PollFormContainer />
</div>
</AutosuggestTextarea>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<UploadFormContainer />
<PollFormContainer />

<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PollButtonContainer />
<div className='compose-form__footer'>
<div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<SpoilerButtonContainer />
<LanguageDropdown />
</div>

<div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
<div className='compose-form__actions'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PollButtonContainer />
<SpoilerButtonContainer />
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div>

<div className='compose-form__submit'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />

<Button
type='submit'
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : messages.publish)}
disabled={!this.canSubmit()}
/>
</div>
</div>
</div>
</div>

<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'>
<Button
type='submit'
text={publishText}
disabled={!this.canSubmit()}
block
/>
</div>
</div>
</form>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import classNames from 'classnames';

import ImmutablePropTypes from 'react-immutable-proptypes';

import { ReactComponent as MoodIcon } from '@material-symbols/svg-600/outlined/mood.svg';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';

import { IconButton } from 'mastodon/components/icon_button';
import { assetHost } from 'mastodon/utils/config';

import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
Expand Down Expand Up @@ -321,7 +323,6 @@ class EmojiPickerDropdown extends PureComponent {
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
};

state = {
Expand Down Expand Up @@ -379,19 +380,20 @@ class EmojiPickerDropdown extends PureComponent {
};

render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading } = this.state;

return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
{button || <img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={`${assetHost}/emoji/1f642.svg`}
/>}
</div>
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown} ref={this.setTargetRef}>
<IconButton
title={title}
aria-expanded={active}
active={active}
iconComponent={MoodIcon}
onClick={this.onToggle}
inverted
/>

<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, placement })=> (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { injectIntl, defineMessages } from 'react-intl';

import classNames from 'classnames';

import { ReactComponent as TranslateIcon } from '@material-symbols/svg-600/outlined/translate.svg';
import { supportsPassiveEvents } from 'detect-passive-events';
import fuzzysort from 'fuzzysort';
import Overlay from 'react-overlays/Overlay';

import { Icon } from 'mastodon/components/icon';
import { languages as preloadedLanguages } from 'mastodon/initial_state';
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';

import TextIconButton from './text_icon_button';

const messages = defineMessages({
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
search: { id: 'compose.language.search', defaultMessage: 'Search languages...' },
Expand Down Expand Up @@ -297,20 +297,24 @@ class LanguageDropdown extends PureComponent {
render () {
const { value, intl, frequentlyUsedLanguages } = this.props;
const { open, placement } = this.state;
const current = preloadedLanguages.find(lang => lang[0] === value);

return (
<div className={classNames('privacy-dropdown', placement, { active: open })}>
<div className='privacy-dropdown__value' ref={this.setTargetRef} >
<TextIconButton
className='privacy-dropdown__value-icon'
label={value && value.toUpperCase()}
title={intl.formatMessage(messages.changeLanguage)}
active={open}
onClick={this.handleToggle}
/>
</div>

<Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
<button
type='button'
title={intl.formatMessage(messages.changeLanguage)}
aria-expanded={open}
onClick={this.handleToggle}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
className={classNames('dropdown-button', { active: open })}
>
<Icon icon={TranslateIcon} />
{current[2]}
</button>

<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
{({ props, placement }) => (
<div {...props}>
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
Expand Down

0 comments on commit a36a1e4

Please sign in to comment.