Reduce composer differences with upstream and simplify code (#2518)

th-new
Claire 2023-12-18 13:20:08 +01:00 committed by GitHub
parent 18856371be
commit 3d3fa75c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 251 deletions

View File

@ -251,9 +251,9 @@ export function submitCompose(routerHistory) {
return;
}
// To make the app more responsive, immediately get the status into the columns
const insertIfOnline = (timelineId) => {
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
@ -662,8 +662,9 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => {
let completion;
if (suggestion.type === 'emoji') {
dispatch(useEmoji(suggestion));
completion = suggestion.native || suggestion.colons;
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
} else if (suggestion.type === 'account') {

View File

@ -56,14 +56,14 @@ class ComposeForm extends ImmutablePureComponent {
isChangingUpload: PropTypes.bool,
isEditing: PropTypes.bool,
isUploading: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
onClearSuggestions: PropTypes.func,
onFetchSuggestions: PropTypes.func,
onSuggestionSelected: PropTypes.func,
onChangeSpoilerText: PropTypes.func,
onPaste: PropTypes.func,
onPickEmoji: PropTypes.func,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
onPickEmoji: PropTypes.func.isRequired,
showSearch: PropTypes.bool,
anyMedia: PropTypes.bool,
isInReply: PropTypes.bool,
@ -77,9 +77,9 @@ class ComposeForm extends ImmutablePureComponent {
spoilersAlwaysOn: PropTypes.bool,
mediaDescriptionConfirmation: PropTypes.bool,
preselectOnReply: PropTypes.bool,
onChangeSpoilerness: PropTypes.func,
onChangeVisibility: PropTypes.func,
onMediaDescriptionConfirm: PropTypes.func,
onChangeSpoilerness: PropTypes.func.isRequired,
onChangeVisibility: PropTypes.func.isRequired,
onMediaDescriptionConfirm: PropTypes.func.isRequired,
...WithOptionalRouterPropTypes
};
@ -100,6 +100,16 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onChange(e.target.value);
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
if (e.keyCode === 13 && e.altKey) {
this.handleSecondarySubmit();
}
};
getFulltextForCharacterCounting = () => {
return [
this.props.spoiler? this.props.spoilerText: '',
@ -116,14 +126,6 @@ class ComposeForm extends ImmutablePureComponent {
};
handleSubmit = (overriddenVisibility = null) => {
const {
onSubmit,
media,
mediaDescriptionConfirmation,
onMediaDescriptionConfirm,
onChangeVisibility,
} = this.props;
if (this.props.text !== this.textareaRef.current.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
@ -135,35 +137,14 @@ class ComposeForm extends ImmutablePureComponent {
}
// Submit unless there are media with missing descriptions
if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
const firstWithoutDescription = media.find(item => !item.get('description'));
onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overriddenVisibility);
} else if (onSubmit) {
if (onChangeVisibility && overriddenVisibility) {
onChangeVisibility(overriddenVisibility);
if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) {
const firstWithoutDescription = this.props.media.find(item => !item.get('description'));
this.props.onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overriddenVisibility);
} else {
if (overriddenVisibility) {
this.props.onChangeVisibility(overriddenVisibility);
}
onSubmit(this.props.history || null);
}
};
// Changes the text value of the spoiler.
handleChangeSpoiler = ({ target: { value } }) => {
const { onChangeSpoilerText } = this.props;
if (onChangeSpoilerText) {
onChangeSpoilerText(value);
}
};
setRef = c => {
this.composeForm = c;
};
// Inserts an emoji at the caret.
handleEmojiPick = (data) => {
const position = this.textareaRef.current.selectionStart;
if (this.props.onPickEmoji) {
this.props.onPickEmoji(position, data);
this.props.onSubmit(this.props.history || null);
}
};
@ -175,30 +156,24 @@ class ComposeForm extends ImmutablePureComponent {
this.handleSubmit(sideArm === 'none' ? null : sideArm);
};
// Selects a suggestion from the autofill.
handleSuggestionSelected = (tokenStart, token, value) => {
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
};
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
};
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
};
handleSpoilerSuggestionSelected = (tokenStart, token, value) => {
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
if (e.keyCode === 13 && e.altKey) {
this.handleSecondarySubmit();
}
};
// Sets a reference to the CW field.
handleRefSpoilerText = (spoilerComponent) => {
if (spoilerComponent) {
this.spoilerText = spoilerComponent.input;
}
handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value);
};
handleFocus = () => {
@ -222,120 +197,99 @@ class ComposeForm extends ImmutablePureComponent {
this._updateFocusAndSelection(prevProps);
}
_updateFocusAndSelection = (prevProps) => {
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end
// of the textbox.
// - Replying to more than one user, selects any usernames past
// the first; this provides a convenient shortcut to drop
// everyone else from the conversation.
_updateFocusAndSelection = (prevProps) => {
const {
spoilerText,
} = this;
const {
focusDate,
caretPosition,
isSubmitting,
preselectDate,
text,
preselectOnReply,
singleColumn,
} = this.props;
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
let selectionEnd, selectionStart;
// Caret/selection handling.
if (focusDate !== prevProps.focusDate) {
switch (true) {
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPosition) && caretPosition !== null:
selectionStart = selectionEnd = caretPosition;
break;
default:
selectionStart = selectionEnd = text.length;
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply && this.props.preselectOnReply) {
selectionEnd = this.props.text.length;
selectionStart = this.props.text.search(/\s/) + 1;
} else if (typeof this.props.caretPosition === 'number') {
selectionStart = this.props.caretPosition;
selectionEnd = this.props.caretPosition;
} else {
selectionEnd = this.props.text.length;
selectionStart = selectionEnd;
}
if (this.textareaRef.current) {
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
this.textareaRef.current.focus();
if (!singleColumn) this.textareaRef.current.scrollIntoView();
if (!this.props.singleColumn) this.textareaRef.current.scrollIntoView();
this.setState({ highlighted: true });
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
}).catch(console.error);
}
// Refocuses the textarea after submitting.
} else if (this.textareaRef.current && prevProps.isSubmitting && !isSubmitting) {
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
this.textareaRef.current.focus();
} else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) {
if (spoilerText) {
spoilerText.focus();
}
} else {
if (this.textareaRef.current) {
this.spoilerText.input.focus();
} else if (prevProps.spoiler) {
this.textareaRef.current.focus();
}
}
}
};
setSpoilerText = (c) => {
this.spoilerText = c;
};
setRef = c => {
this.composeForm = c;
};
handleEmojiPick = (data) => {
const position = this.textareaRef.current.selectionStart;
this.props.onPickEmoji(position, data);
};
render () {
const {
handleEmojiPick,
handleSecondarySubmit,
handleSubmit,
} = this;
const {
advancedOptions,
intl,
advancedOptions,
isSubmitting,
layout,
onChangeSpoilerness,
onClearSuggestions,
onFetchSuggestions,
onPaste,
privacy,
sensitive,
showSearch,
sideArm,
spoiler,
spoilerText,
suggestions,
spoilersAlwaysOn,
isEditing,
} = this.props;
const { highlighted } = this.state;
const countText = this.getFulltextForCharacterCounting();
const disabled = this.props.isSubmitting;
return (
<div className='compose-form'>
<form className='compose-form' onSubmit={this.handleSubmit}>
<WarningContainer />
<ReplyIndicatorContainer />
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<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={spoilerText}
onChange={this.handleChangeSpoiler}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!spoiler}
ref={this.handleRefSpoilerText}
suggestions={suggestions}
onSuggestionsFetchRequested={onFetchSuggestions}
onSuggestionsClearRequested={onClearSuggestions}
onSuggestionSelected={this.handleSpoilerSuggestionSelected}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='glitch.composer.spoiler.input'
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
autoFocus={false}
@ -347,15 +301,15 @@ class ComposeForm extends ImmutablePureComponent {
<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={isSubmitting}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
suggestions={suggestions}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onSuggestionsFetchRequested={onFetchSuggestions}
onSuggestionsClearRequested={onClearSuggestions}
onSuggestionSelected={this.handleSuggestionSelected}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
lang={this.props.lang}
@ -366,34 +320,33 @@ class ComposeForm extends ImmutablePureComponent {
<PollFormContainer />
</div>
</AutosuggestTextarea>
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__buttons-wrapper'>
<OptionsContainer
advancedOptions={advancedOptions}
disabled={isSubmitting}
onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
onToggleSpoiler={this.props.spoilersAlwaysOn ? null : onChangeSpoilerness}
onUpload={onPaste}
isEditing={isEditing}
sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
sensitive={sensitive || (spoilersAlwaysOn && this.props.spoilerText && this.props.spoilerText.length > 0)}
spoiler={spoilersAlwaysOn ? (this.props.spoilerText && this.props.spoilerText.length > 0) : this.props.spoiler}
/>
<div className='character-counter__wrapper'>
<CharacterCounter text={countText} max={maxChars} />
<CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} />
</div>
</div>
</div>
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
onSecondarySubmit={this.handleSecondarySubmit}
onSubmit={this.handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
</form>
);
}

View File

@ -30,9 +30,11 @@ export default class NavigationBar extends ImmutablePureComponent {
</Permalink>
<div className='navigation-bar__profile'>
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<span>
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${username}`}>
<strong className='navigation-bar__profile-account'>@{username}</strong>
</Permalink>
</span>
{ profileLink !== undefined && (
<a

View File

@ -1,4 +1,3 @@
// Package imports.
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
@ -9,8 +8,6 @@ import { connect } from 'react-redux';
import Toggle from 'react-toggle';
// Components.
import { IconButton } from 'flavours/glitch/components/icon_button';
import { pollLimits } from 'flavours/glitch/initial_state';
@ -20,11 +17,6 @@ import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import TextIconButton from './text_icon_button';
// Utils.
// Messages.
const messages = defineMessages({
advanced_options_icon_title: {
defaultMessage: 'Advanced options',
@ -133,12 +125,12 @@ class ComposerOptions extends ImmutablePureComponent {
allowPoll: PropTypes.bool,
hasPoll: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChangeAdvancedOption: PropTypes.func,
onChangeContentType: PropTypes.func,
onTogglePoll: PropTypes.func,
onDoodleOpen: PropTypes.func,
onChangeAdvancedOption: PropTypes.func.isRequired,
onChangeContentType: PropTypes.func.isRequired,
onTogglePoll: PropTypes.func.isRequired,
onDoodleOpen: PropTypes.func.isRequired,
onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func,
onUpload: PropTypes.func.isRequired,
contentType: PropTypes.string,
resetFileKey: PropTypes.number,
spoiler: PropTypes.bool,
@ -146,20 +138,17 @@ class ComposerOptions extends ImmutablePureComponent {
isEditing: PropTypes.bool,
};
// Handles file selection.
handleChangeFiles = ({ target: { files } }) => {
const { onUpload } = this.props;
if (files.length && onUpload) {
if (files.length) {
onUpload(files);
}
};
// Handles attachment clicks.
handleClickAttach = (name) => {
const { fileElement } = this;
const { onDoodleOpen } = this.props;
// We switch over the name of the option.
switch (name) {
case 'upload':
if (fileElement) {
@ -167,14 +156,11 @@ class ComposerOptions extends ImmutablePureComponent {
}
return;
case 'doodle':
if (onDoodleOpen) {
onDoodleOpen();
}
return;
}
};
// Handles a ref to the file input.
handleRefFileElement = (fileElement) => {
this.fileElement = fileElement;
};
@ -186,7 +172,6 @@ class ComposerOptions extends ImmutablePureComponent {
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
};
// Rendering.
render () {
const {
acceptContentTypes,
@ -290,7 +275,7 @@ class ComposerOptions extends ImmutablePureComponent {
{onToggleSpoiler && (
<TextIconButton
active={spoiler}
ariaControls='glitch.composer.spoiler.input'
ariaControls='cw-spoiler-input'
label='CW'
onClick={onToggleSpoiler}
title={formatMessage(messages.spoiler)}

View File

@ -2,15 +2,10 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { maxChars } from 'flavours/glitch/initial_state';
const messages = defineMessages({
publish: {
@ -31,7 +26,6 @@ const messages = defineMessages({
class Publisher extends ImmutablePureComponent {
static propTypes = {
countText: PropTypes.string,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
onSecondarySubmit: PropTypes.func,
@ -46,13 +40,7 @@ class Publisher extends ImmutablePureComponent {
};
render () {
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
const diff = maxChars - length(countText || '');
const computedClass = classNames('compose-form__publish', {
disabled: disabled,
over: diff < 0,
});
const { disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
@ -78,8 +66,8 @@ class Publisher extends ImmutablePureComponent {
};
return (
<div className={computedClass}>
{sideArm && !isEditing && sideArm !== 'none' ? (
<div className='compose-form__publish'>
{sideArm && !isEditing && sideArm !== 'none' && (
<div className='compose-form__publish-button-wrapper'>
<Button
className='side_arm'
@ -90,7 +78,7 @@ class Publisher extends ImmutablePureComponent {
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[sideArm])}`}
/>
</div>
) : null}
)}
<div className='compose-form__publish-button-wrapper'>
<Button
className='primary'

View File

@ -18,13 +18,11 @@ export default class UploadForm extends ImmutablePureComponent {
<div className='compose-form__upload-wrapper'>
<UploadProgressContainer />
{mediaIds.size > 0 && (
<div className='compose-form__uploads-wrapper'>
{mediaIds.map(id => (
<UploadContainer id={id} key={id} />
))}
</div>
)}
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
</div>

View File

@ -37,9 +37,7 @@ const messages = defineMessages({
},
});
// State mapping.
function mapStateToProps (state) {
const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
const sideArmPrivacy = state => {
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null;
const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']);
@ -53,44 +51,44 @@ function mapStateToProps (state) {
sideArmPrivacy = sideArmRestrictedPrivacy;
break;
}
sideArmPrivacy = sideArmPrivacy || sideArmBasePrivacy;
return {
advancedOptions: state.getIn(['compose', 'advanced_options']),
return sideArmPrivacy || sideArmBasePrivacy;
};
const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['local_settings', 'always_show_spoilers_field']) || state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isEditing: state.getIn(['compose', 'id']) !== null,
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
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']),
sideArm: sideArmPrivacy,
sensitive: state.getIn(['compose', 'sensitive']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
suggestions: state.getIn(['compose', 'suggestions']),
text: state.getIn(['compose', 'text']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
spoilersAlwaysOn: spoilersAlwaysOn,
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
};
}
advancedOptions: state.getIn(['compose', 'advanced_options']),
layout: state.getIn(['local_settings', 'layout']),
media: state.getIn(['compose', 'media_attachments']),
sideArm: sideArmPrivacy(state),
sensitive: state.getIn(['compose', 'sensitive']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
spoilersAlwaysOn: state.getIn(['local_settings', 'always_show_spoilers_field']),
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
});
// Dispatch mapping.
const mapDispatchToProps = (dispatch, { intl }) => ({
onChange (text) {
dispatch(changeCompose(text));
},
onSubmit(routerHistory) {
dispatch(submitCompose(routerHistory));
onSubmit (router) {
dispatch(submitCompose(router));
},
onClearSuggestions () {