Switch glitch-soc to upstream's old composer
parent
10a0d76bf0
commit
7586d4348f
|
@ -59,7 +59,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
|||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
||||
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
||||
|
||||
|
@ -148,13 +148,13 @@ export function resetCompose() {
|
|||
};
|
||||
}
|
||||
|
||||
export const focusCompose = (routerHistory, defaultText) => dispatch => {
|
||||
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(routerHistory);
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
|
||||
export function mentionCompose(account, routerHistory) {
|
||||
|
@ -246,11 +246,6 @@ export function submitCompose(routerHistory) {
|
|||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
// If the response has no data then we can't do anything else.
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To make the app more responsive, immediately push the status
|
||||
// into the columns
|
||||
const insertIfOnline = timelineId => {
|
||||
|
@ -660,15 +655,19 @@ export const readyComposeSuggestionsTags = (token, tags) => ({
|
|||
|
||||
export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||
return (dispatch, getState) => {
|
||||
let completion;
|
||||
let completion, startPosition;
|
||||
|
||||
if (suggestion.type === 'emoji') {
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
} else if (suggestion.type === 'hashtag') {
|
||||
completion = `#${suggestion.name}`;
|
||||
completion = `#${suggestion.name}`;
|
||||
startPosition = position - 1;
|
||||
} else if (suggestion.type === 'account') {
|
||||
completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
|
||||
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
|
||||
startPosition = position;
|
||||
}
|
||||
|
||||
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||
|
@ -676,7 +675,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||
if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
|
||||
dispatch({
|
||||
type: COMPOSE_SUGGESTION_SELECT,
|
||||
position,
|
||||
position: startPosition,
|
||||
token,
|
||||
completion,
|
||||
path,
|
||||
|
@ -684,7 +683,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||
} else {
|
||||
dispatch({
|
||||
type: COMPOSE_SUGGESTION_IGNORE,
|
||||
position,
|
||||
position: startPosition,
|
||||
token,
|
||||
completion,
|
||||
path,
|
||||
|
@ -786,18 +785,26 @@ export function changeComposeVisibility(value) {
|
|||
};
|
||||
}
|
||||
|
||||
export function changeComposeContentType(value) {
|
||||
return {
|
||||
type: COMPOSE_CONTENT_TYPE_CHANGE,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function insertEmojiCompose(position, emoji) {
|
||||
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||
return {
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
position,
|
||||
emoji,
|
||||
needsSpace,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeComposing(value) {
|
||||
return {
|
||||
type: COMPOSE_COMPOSING_CHANGE,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeComposeContentType(value) {
|
||||
return {
|
||||
type: COMPOSE_CONTENT_TYPE_CHANGE,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||
import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
||||
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
|
||||
|
@ -44,8 +42,8 @@ class ActionBar extends PureComponent {
|
|||
|
||||
let menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
|
|
|
@ -16,7 +16,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='autosuggest-account' title={account.get('acct')}>
|
||||
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
|
||||
<DisplayName account={account} inline />
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,35 +10,36 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
|
||||
import { length } from 'stringz';
|
||||
|
||||
import { maxChars } from 'flavours/glitch/initial_state';
|
||||
import { isMobile } from 'flavours/glitch/is_mobile';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import { Button } from '../../../components/button';
|
||||
import { maxChars } from '../../../initial_state';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import OptionsContainer from '../containers/options_container';
|
||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
import { countableText } from '../util/counter';
|
||||
|
||||
import CharacterCounter from './character_counter';
|
||||
import Publisher from './publisher';
|
||||
import TextareaIcons from './textarea_icons';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
missingDescriptionMessage: {
|
||||
id: 'confirmations.missing_media_description.message',
|
||||
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.',
|
||||
},
|
||||
missingDescriptionConfirm: {
|
||||
id: 'confirmations.missing_media_description.confirm',
|
||||
defaultMessage: 'Send anyway',
|
||||
},
|
||||
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' },
|
||||
});
|
||||
|
||||
class ComposeForm extends ImmutablePureComponent {
|
||||
|
@ -64,26 +65,16 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
showSearch: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
anyMedia: PropTypes.bool,
|
||||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
media: ImmutablePropTypes.list,
|
||||
sideArm: PropTypes.string,
|
||||
sensitive: PropTypes.bool,
|
||||
spoilersAlwaysOn: PropTypes.bool,
|
||||
mediaDescriptionConfirmation: PropTypes.bool,
|
||||
preselectOnReply: PropTypes.bool,
|
||||
onChangeSpoilerness: PropTypes.func.isRequired,
|
||||
onChangeVisibility: PropTypes.func.isRequired,
|
||||
onMediaDescriptionConfirm: PropTypes.func.isRequired,
|
||||
...WithOptionalRouterPropTypes
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showSearch: false,
|
||||
autoFocus: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -103,28 +94,21 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
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: '',
|
||||
countableText(this.props.text),
|
||||
this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '',
|
||||
].join('');
|
||||
return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join('');
|
||||
};
|
||||
|
||||
canSubmit = () => {
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
|
||||
const fulltext = this.getFulltextForCharacterCounting();
|
||||
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
||||
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia));
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia));
|
||||
};
|
||||
|
||||
handleSubmit = (e, overriddenVisibility = null) => {
|
||||
handleSubmit = (e) => {
|
||||
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,26 +119,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(this.props.history || null);
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit unless there are media with missing descriptions
|
||||
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);
|
||||
}
|
||||
this.props.onSubmit(this.props.history || null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the secondary submit button.
|
||||
handleSecondarySubmit = () => {
|
||||
const { sideArm } = this.props;
|
||||
this.handleSubmit(null, sideArm === 'none' ? null : sideArm);
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
|
@ -207,7 +176,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
|
||||
let selectionEnd, selectionStart;
|
||||
|
||||
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply && this.props.preselectOnReply) {
|
||||
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
|
||||
selectionEnd = this.props.text.length;
|
||||
selectionStart = this.props.text.search(/\s/) + 1;
|
||||
} else if (typeof this.props.caretPosition === 'number') {
|
||||
|
@ -224,7 +193,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
Promise.resolve().then(() => {
|
||||
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.textareaRef.current.focus();
|
||||
if (!this.props.singleColumn) this.textareaRef.current.scrollIntoView();
|
||||
this.setState({ highlighted: true });
|
||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||
}).catch(console.error);
|
||||
|
@ -248,28 +216,28 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
const position = this.textareaRef.current.selectionStart;
|
||||
const { text } = this.props;
|
||||
const position = this.textareaRef.current.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
|
||||
this.props.onPickEmoji(position, data);
|
||||
this.props.onPickEmoji(position, data, needsSpace);
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
intl,
|
||||
advancedOptions,
|
||||
isSubmitting,
|
||||
onChangeSpoilerness,
|
||||
onPaste,
|
||||
privacy,
|
||||
sensitive,
|
||||
showSearch,
|
||||
sideArm,
|
||||
spoilersAlwaysOn,
|
||||
isEditing,
|
||||
} = this.props;
|
||||
const { intl, onPaste, autoFocus } = this.props;
|
||||
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 />
|
||||
|
@ -292,7 +260,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
lang={this.props.lang}
|
||||
autoFocus={false}
|
||||
spellCheck
|
||||
/>
|
||||
</div>
|
||||
|
@ -311,10 +278,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
||||
autoFocus={autoFocus}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<TextareaIcons advancedOptions={advancedOptions} />
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
|
@ -323,28 +289,30 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<OptionsContainer
|
||||
advancedOptions={advancedOptions}
|
||||
disabled={isSubmitting}
|
||||
onToggleSpoiler={this.props.spoilersAlwaysOn ? null : onChangeSpoilerness}
|
||||
onUpload={onPaste}
|
||||
isEditing={isEditing}
|
||||
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='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||
<SpoilerButtonContainer />
|
||||
<LanguageDropdown />
|
||||
</div>
|
||||
|
||||
<div className='character-counter__wrapper'>
|
||||
<CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Publisher
|
||||
disabled={!this.canSubmit()}
|
||||
isEditing={isEditing}
|
||||
onSecondarySubmit={this.handleSecondarySubmit}
|
||||
privacy={privacy}
|
||||
sideArm={sideArm}
|
||||
/>
|
||||
<div className='compose-form__publish'>
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
type='submit'
|
||||
text={publishText}
|
||||
disabled={!this.canSubmit()}
|
||||
block
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
// Components.
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
import DropdownMenu from './dropdown_menu';
|
||||
|
||||
// The component.
|
||||
export default class ComposerOptionsDropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
meta: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
})).isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
container: PropTypes.func,
|
||||
renderItemContents: PropTypes.func,
|
||||
closeOnChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
closeOnChange: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
openedViaKeyboard: undefined,
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
// Toggles opening and closing the dropdown.
|
||||
handleToggle = ({ type }) => {
|
||||
const { onModalOpen } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
const modal = this.handleMakeModal();
|
||||
if (modal && onModalOpen) {
|
||||
onModalOpen(modal);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !open, openedViaKeyboard: type !== 'click' });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleToggle(e);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleItemClick = (e) => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onModalClose,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
const { name } = items[i];
|
||||
|
||||
e.preventDefault(); // Prevents focus from changing
|
||||
if (closeOnChange) onModalClose();
|
||||
onChange(name);
|
||||
};
|
||||
|
||||
// Creates an action modal object.
|
||||
handleMakeModal = () => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onModalOpen,
|
||||
onModalClose,
|
||||
value,
|
||||
} = this.props;
|
||||
|
||||
// Required props.
|
||||
if (!(onChange && onModalOpen && onModalClose && items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The object.
|
||||
return {
|
||||
renderItemContents: this.props.renderItemContents,
|
||||
onClick: this.handleItemClick,
|
||||
actions: items.map(
|
||||
({
|
||||
name,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
active: value && name === value,
|
||||
name,
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
disabled,
|
||||
title,
|
||||
icon,
|
||||
iconComponent,
|
||||
items,
|
||||
onChange,
|
||||
value,
|
||||
container,
|
||||
renderItemContents,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('privacy-dropdown', placement, { active: open })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.setTargetRef}
|
||||
>
|
||||
<IconButton
|
||||
active={open}
|
||||
className='privacy-dropdown__value-icon'
|
||||
disabled={disabled}
|
||||
icon={icon}
|
||||
iconComponent={iconComponent}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
size={18}
|
||||
style={{
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
<Overlay
|
||||
containerPadding={20}
|
||||
placement={placement}
|
||||
show={open}
|
||||
flip
|
||||
target={this.findTarget}
|
||||
container={container}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
renderItemContents={renderItemContents}
|
||||
onChange={onChange}
|
||||
onClose={this.handleClose}
|
||||
value={value}
|
||||
openedViaKeyboard={this.state.openedViaKeyboard}
|
||||
closeOnChange={closeOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
|
||||
// Components.
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
||||
// The component.
|
||||
export default class ComposerOptionsDropdownContent extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
meta: PropTypes.node,
|
||||
name: PropTypes.string.isRequired,
|
||||
text: PropTypes.node,
|
||||
})),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
renderItemContents: PropTypes.func,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
closeOnChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
closeOnChange: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined,
|
||||
};
|
||||
|
||||
// When the document is clicked elsewhere, we close the dropdown.
|
||||
handleDocumentClick = (e) => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Stores our node in `this.node`.
|
||||
setRef = (node) => {
|
||||
this.node = node;
|
||||
};
|
||||
|
||||
// On mounting, we add our listeners.
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) {
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
} else {
|
||||
this.node.firstChild.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
// On unmounting, we remove our listeners.
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
const {
|
||||
onChange,
|
||||
onClose,
|
||||
closeOnChange,
|
||||
items,
|
||||
} = this.props;
|
||||
|
||||
const { name } = items[i];
|
||||
|
||||
e.preventDefault(); // Prevents change in focus on click
|
||||
if (closeOnChange) {
|
||||
onClose();
|
||||
}
|
||||
onChange(name);
|
||||
};
|
||||
|
||||
// Handle changes differently whether the dropdown is a list of options or actions
|
||||
handleChange = (name) => {
|
||||
if (this.props.value) {
|
||||
this.props.onChange(name);
|
||||
} else {
|
||||
this.setState({ value: name });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
const index = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const { items } = this.props;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(items[Number(element.getAttribute('data-index'))].name);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
|
||||
renderItem = (item, i) => {
|
||||
const { name, icon, iconComponent, meta, text } = item;
|
||||
|
||||
const active = (name === (this.props.value || this.state.value));
|
||||
|
||||
const computedClass = classNames('privacy-dropdown__option', { active });
|
||||
|
||||
let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
|
||||
|
||||
if (!contents) {
|
||||
contents = (
|
||||
<>
|
||||
{icon && (
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon className='icon' id={icon} icon={iconComponent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={computedClass}
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
role='option'
|
||||
aria-selected={active}
|
||||
tabIndex={0}
|
||||
key={name}
|
||||
data-index={i}
|
||||
ref={active ? this.setFocusRef : null}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
items,
|
||||
style,
|
||||
} = this.props;
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<div style={{ ...style }} role='listbox' ref={this.setRef}>
|
||||
{!!items && items.map((item, i) => this.renderItem(item, i))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
||||
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
|
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { signOutLink } from 'flavours/glitch/utils/backend_links';
|
||||
import { conditionalRender } from 'flavours/glitch/utils/react_helpers';
|
||||
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
columns: ImmutablePropTypes.list,
|
||||
unreadNotifications: PropTypes.number,
|
||||
showNotificationsBadge: PropTypes.bool,
|
||||
intl: PropTypes.object,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLogoutClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onLogout();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props;
|
||||
|
||||
// Only renders the component if the column isn't being shown.
|
||||
const renderForColumn = conditionalRender.bind(null,
|
||||
columnId => !columns || !columns.some(
|
||||
column => column.get('id') === columnId,
|
||||
),
|
||||
);
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<nav className='drawer__header'>
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.start)}
|
||||
title={intl.formatMessage(messages.start)}
|
||||
to='/getting-started'
|
||||
className='drawer__tab'
|
||||
><Icon id='bars' icon={MenuIcon} /></Link>
|
||||
{renderForColumn('HOME', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.home_timeline)}
|
||||
title={intl.formatMessage(messages.home_timeline)}
|
||||
to='/home'
|
||||
className='drawer__tab'
|
||||
><Icon id='home' icon={HomeIcon} /></Link>
|
||||
))}
|
||||
{renderForColumn('NOTIFICATIONS', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.notifications)}
|
||||
title={intl.formatMessage(messages.notifications)}
|
||||
to='/notifications'
|
||||
className='drawer__tab'
|
||||
>
|
||||
<span className='icon-badge-wrapper'>
|
||||
<Icon id='bell' icon={NotificationsIcon} />
|
||||
{ showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
{renderForColumn('COMMUNITY', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.community)}
|
||||
title={intl.formatMessage(messages.community)}
|
||||
to='/public/local'
|
||||
className='drawer__tab'
|
||||
><Icon id='users' icon={PeopleIcon} /></Link>
|
||||
))}
|
||||
{renderForColumn('PUBLIC', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.public)}
|
||||
title={intl.formatMessage(messages.public)}
|
||||
to='/public'
|
||||
className='drawer__tab'
|
||||
><Icon id='globe' icon={PublicIcon} /></Link>
|
||||
))}
|
||||
<a
|
||||
aria-label={intl.formatMessage(messages.settings)}
|
||||
onClick={onSettingsClick}
|
||||
href='/settings/preferences'
|
||||
title={intl.formatMessage(messages.settings)}
|
||||
className='drawer__tab'
|
||||
><Icon id='cogs' icon={ManufacturingIcon} /></a>
|
||||
<a
|
||||
aria-label={intl.formatMessage(messages.logout)}
|
||||
onClick={this.handleLogoutClick}
|
||||
href={signOutLink}
|
||||
title={intl.formatMessage(messages.logout)}
|
||||
className='drawer__tab'
|
||||
><Icon id='sign-out' icon={LogoutIcon} /></a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(Header);
|
|
@ -2,12 +2,11 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { profileLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
|
||||
import ActionBar from './action_bar';
|
||||
|
@ -24,24 +23,21 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||
const username = this.props.account.get('acct');
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink className='avatar' href={this.props.account.get('url')} to={`/@${username}`}>
|
||||
<Link to={`/@${username}`}>
|
||||
<span style={{ display: 'none' }}>{username}</span>
|
||||
<Avatar account={this.props.account} size={46} />
|
||||
</Permalink>
|
||||
</Link>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<span>
|
||||
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${username}`}>
|
||||
<Link to={`/@${username}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
||||
</Permalink>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
{ profileLink !== undefined && (
|
||||
<a
|
||||
href={profileLink}
|
||||
className='navigation-bar__profile-edit'
|
||||
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
)}
|
||||
<span>
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
|
|
|
@ -1,330 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import AttachFileIcon from '@/material-icons/400-24px/attach_file.svg?react';
|
||||
import BrushIcon from '@/material-icons/400-24px/brush.svg?react';
|
||||
import CodeIcon from '@/material-icons/400-24px/code.svg?react';
|
||||
import DescriptionIcon from '@/material-icons/400-24px/description.svg?react';
|
||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||
import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { pollLimits } from 'flavours/glitch/initial_state';
|
||||
|
||||
|
||||
import DropdownContainer from '../containers/dropdown_container';
|
||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
|
||||
import TextIconButton from './text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
advanced_options_icon_title: {
|
||||
defaultMessage: 'Advanced options',
|
||||
id: 'advanced_options.icon_title',
|
||||
},
|
||||
attach: {
|
||||
defaultMessage: 'Attach...',
|
||||
id: 'compose.attach',
|
||||
},
|
||||
content_type: {
|
||||
defaultMessage: 'Content type',
|
||||
id: 'content-type.change',
|
||||
},
|
||||
doodle: {
|
||||
defaultMessage: 'Draw something',
|
||||
id: 'compose.attach.doodle',
|
||||
},
|
||||
html: {
|
||||
defaultMessage: 'HTML',
|
||||
id: 'compose.content-type.html',
|
||||
},
|
||||
local_only_long: {
|
||||
defaultMessage: 'Do not post to other instances',
|
||||
id: 'advanced_options.local-only.long',
|
||||
},
|
||||
local_only_short: {
|
||||
defaultMessage: 'Local-only',
|
||||
id: 'advanced_options.local-only.short',
|
||||
},
|
||||
markdown: {
|
||||
defaultMessage: 'Markdown',
|
||||
id: 'compose.content-type.markdown',
|
||||
},
|
||||
plain: {
|
||||
defaultMessage: 'Plain text',
|
||||
id: 'compose.content-type.plain',
|
||||
},
|
||||
spoiler: {
|
||||
defaultMessage: 'Hide text behind warning',
|
||||
id: 'compose_form.spoiler',
|
||||
},
|
||||
threaded_mode_long: {
|
||||
defaultMessage: 'Automatically opens a reply on posting',
|
||||
id: 'advanced_options.threaded_mode.long',
|
||||
},
|
||||
threaded_mode_short: {
|
||||
defaultMessage: 'Threaded mode',
|
||||
id: 'advanced_options.threaded_mode.short',
|
||||
},
|
||||
upload: {
|
||||
defaultMessage: 'Upload a file',
|
||||
id: 'compose.attach.upload',
|
||||
},
|
||||
add_poll: {
|
||||
defaultMessage: 'Add a poll',
|
||||
id: 'poll_button.add_poll',
|
||||
},
|
||||
remove_poll: {
|
||||
defaultMessage: 'Remove poll',
|
||||
id: 'poll_button.remove_poll',
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { name }) => ({
|
||||
checked: state.getIn(['compose', 'advanced_options', name]),
|
||||
});
|
||||
|
||||
class ToggleOptionImpl extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onChangeAdvancedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = () => {
|
||||
this.props.onChangeAdvancedOption(this.props.name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { meta, text, checked } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Toggle checked={checked} onChange={this.handleChange} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ToggleOption = connect(mapStateToProps)(ToggleOptionImpl);
|
||||
|
||||
class ComposerOptions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
disabled: PropTypes.bool,
|
||||
allowMedia: PropTypes.bool,
|
||||
allowPoll: PropTypes.bool,
|
||||
hasPoll: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeAdvancedOption: PropTypes.func.isRequired,
|
||||
onChangeContentType: PropTypes.func.isRequired,
|
||||
onTogglePoll: PropTypes.func.isRequired,
|
||||
onDoodleOpen: PropTypes.func.isRequired,
|
||||
onToggleSpoiler: PropTypes.func,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
contentType: PropTypes.string,
|
||||
resetFileKey: PropTypes.number,
|
||||
spoiler: PropTypes.bool,
|
||||
showContentTypeChoice: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleChangeFiles = ({ target: { files } }) => {
|
||||
const { onUpload } = this.props;
|
||||
if (files.length) {
|
||||
onUpload(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClickAttach = (name) => {
|
||||
const { fileElement } = this;
|
||||
const { onDoodleOpen } = this.props;
|
||||
|
||||
switch (name) {
|
||||
case 'upload':
|
||||
if (fileElement) {
|
||||
fileElement.click();
|
||||
}
|
||||
return;
|
||||
case 'doodle':
|
||||
onDoodleOpen();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
handleRefFileElement = (fileElement) => {
|
||||
this.fileElement = fileElement;
|
||||
};
|
||||
|
||||
renderToggleItemContents = (item) => {
|
||||
const { onChangeAdvancedOption } = this.props;
|
||||
const { name, meta, text } = item;
|
||||
|
||||
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
contentType,
|
||||
disabled,
|
||||
allowMedia,
|
||||
allowPoll,
|
||||
hasPoll,
|
||||
onChangeAdvancedOption,
|
||||
onChangeContentType,
|
||||
onTogglePoll,
|
||||
onToggleSpoiler,
|
||||
resetFileKey,
|
||||
spoiler,
|
||||
showContentTypeChoice,
|
||||
isEditing,
|
||||
intl: { formatMessage },
|
||||
} = this.props;
|
||||
|
||||
const contentTypeItems = {
|
||||
plain: {
|
||||
icon: 'file-text',
|
||||
iconComponent: DescriptionIcon,
|
||||
name: 'text/plain',
|
||||
text: formatMessage(messages.plain),
|
||||
},
|
||||
html: {
|
||||
icon: 'code',
|
||||
iconComponent: CodeIcon,
|
||||
name: 'text/html',
|
||||
text: formatMessage(messages.html),
|
||||
},
|
||||
markdown: {
|
||||
icon: 'arrow-circle-down',
|
||||
iconComponent: MarkdownIcon,
|
||||
name: 'text/markdown',
|
||||
text: formatMessage(messages.markdown),
|
||||
},
|
||||
};
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<div className='compose-form__buttons'>
|
||||
<input
|
||||
accept={acceptContentTypes}
|
||||
disabled={disabled || !allowMedia}
|
||||
key={resetFileKey}
|
||||
onChange={this.handleChangeFiles}
|
||||
ref={this.handleRefFileElement}
|
||||
type='file'
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<DropdownContainer
|
||||
disabled={disabled || !allowMedia}
|
||||
icon='paperclip'
|
||||
iconComponent={AttachFileIcon}
|
||||
items={[
|
||||
{
|
||||
icon: 'cloud-upload',
|
||||
iconComponent: UploadFileIcon,
|
||||
name: 'upload',
|
||||
text: formatMessage(messages.upload),
|
||||
},
|
||||
{
|
||||
icon: 'paint-brush',
|
||||
iconComponent: BrushIcon,
|
||||
name: 'doodle',
|
||||
text: formatMessage(messages.doodle),
|
||||
},
|
||||
]}
|
||||
onChange={this.handleClickAttach}
|
||||
title={formatMessage(messages.attach)}
|
||||
/>
|
||||
{!!pollLimits && (
|
||||
<IconButton
|
||||
active={hasPoll}
|
||||
disabled={disabled || !allowPoll}
|
||||
icon='tasks'
|
||||
iconComponent={InsertChartIcon}
|
||||
inverted
|
||||
onClick={onTogglePoll}
|
||||
size={18}
|
||||
style={{
|
||||
height: null,
|
||||
lineHeight: null,
|
||||
}}
|
||||
title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
|
||||
/>
|
||||
)}
|
||||
<PrivacyDropdownContainer disabled={disabled || isEditing} />
|
||||
{showContentTypeChoice && (
|
||||
<DropdownContainer
|
||||
disabled={disabled}
|
||||
icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
|
||||
iconComponent={(contentTypeItems[contentType.split('/')[1]] || {}).iconComponent}
|
||||
items={[
|
||||
contentTypeItems.plain,
|
||||
contentTypeItems.html,
|
||||
contentTypeItems.markdown,
|
||||
]}
|
||||
onChange={onChangeContentType}
|
||||
title={formatMessage(messages.content_type)}
|
||||
value={contentType}
|
||||
/>
|
||||
)}
|
||||
{onToggleSpoiler && (
|
||||
<TextIconButton
|
||||
active={spoiler}
|
||||
ariaControls='cw-spoiler-input'
|
||||
label='CW'
|
||||
onClick={onToggleSpoiler}
|
||||
title={formatMessage(messages.spoiler)}
|
||||
/>
|
||||
)}
|
||||
<LanguageDropdown />
|
||||
<DropdownContainer
|
||||
disabled={disabled || isEditing}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
items={advancedOptions ? [
|
||||
{
|
||||
meta: formatMessage(messages.local_only_long),
|
||||
name: 'do_not_federate',
|
||||
text: formatMessage(messages.local_only_short),
|
||||
},
|
||||
{
|
||||
meta: formatMessage(messages.threaded_mode_long),
|
||||
name: 'threaded_mode',
|
||||
text: formatMessage(messages.threaded_mode_short),
|
||||
},
|
||||
] : null}
|
||||
onChange={onChangeAdvancedOption}
|
||||
renderItemContents={this.renderToggleItemContents}
|
||||
title={formatMessage(messages.advanced_options_icon_title)}
|
||||
closeOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ComposerOptions);
|
|
@ -0,0 +1,61 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
||||
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
};
|
||||
|
||||
class PollButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, active, unavailable, disabled } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-button'>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
iconComponent={InsertChartIcon}
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
className={`compose-form__poll-button-icon ${active ? 'active' : ''}`}
|
||||
size={18}
|
||||
inverted
|
||||
style={iconStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(PollButton);
|
|
@ -8,21 +8,19 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AutosuggestInput from 'flavours/glitch/components/autosuggest_input';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { pollLimits } from 'flavours/glitch/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' },
|
||||
multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' },
|
||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
|
@ -38,6 +36,7 @@ class OptionIntl extends PureComponent {
|
|||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
|
@ -53,6 +52,19 @@ class OptionIntl extends PureComponent {
|
|||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
handleCheckboxKeypress = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleToggleMultiple(e);
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
};
|
||||
|
@ -71,11 +83,19 @@ class OptionIntl extends PureComponent {
|
|||
return (
|
||||
<li>
|
||||
<label className='poll__option editable'>
|
||||
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
onKeyPress={this.handleCheckboxKeypress}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
/>
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={pollLimits.max_option_chars}
|
||||
maxLength={100}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
|
@ -126,8 +146,8 @@ class PollForm extends ImmutablePureComponent {
|
|||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleSelectMultiple = e => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true');
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
|
@ -142,21 +162,11 @@ class PollForm extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
{options.size < pollLimits.max_options && (
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input')} style={{ opacity: 0 }} />
|
||||
<button className='button button-secondary' onClick={this.handleAddOption} type='button'><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
</label>
|
||||
)}
|
||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}>
|
||||
<option value='false'>{intl.formatMessage(messages.single_choice)}</option>
|
||||
<option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
|
||||
</select>
|
||||
<button type='button' disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
|
|
|
@ -3,12 +3,19 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
import Dropdown from './dropdown';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
|
@ -22,6 +29,120 @@ const messages = defineMessages({
|
|||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
||||
class PrivacyDropdownMenu extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props;
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
return (
|
||||
<div style={{ ...style }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon id={item.icon} icon={item.iconComponent} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PrivacyDropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -36,62 +157,139 @@ class PrivacyDropdown extends PureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, isUserTouching, intl: { formatMessage } } = this.props;
|
||||
state = {
|
||||
open: false,
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
// We predefine our privacy items so that we can easily pick the
|
||||
// dropdown icon later.
|
||||
const privacyItems = {
|
||||
direct: {
|
||||
icon: 'envelope',
|
||||
iconComponent: MailIcon,
|
||||
meta: formatMessage(messages.direct_long),
|
||||
name: 'direct',
|
||||
text: formatMessage(messages.direct_short),
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
iconComponent: LockIcon,
|
||||
meta: formatMessage(messages.private_long),
|
||||
name: 'private',
|
||||
text: formatMessage(messages.private_short),
|
||||
},
|
||||
public: {
|
||||
icon: 'globe',
|
||||
iconComponent: PublicIcon,
|
||||
meta: formatMessage(messages.public_long),
|
||||
name: 'public',
|
||||
text: formatMessage(messages.public_short),
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
iconComponent: LockOpenIcon,
|
||||
meta: formatMessage(messages.unlisted_long),
|
||||
name: 'unlisted',
|
||||
text: formatMessage(messages.unlisted_short),
|
||||
},
|
||||
};
|
||||
|
||||
const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private];
|
||||
|
||||
if (!noDirect) {
|
||||
items.push(privacyItems.direct);
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleChange = value => {
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', iconComponent: LockOpenIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
];
|
||||
|
||||
if (!this.props.noDirect) {
|
||||
this.options.push(
|
||||
{ icon: 'at', iconComponent: AlternateEmailIcon, value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, container, disabled, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
icon={(privacyItems[value] || {}).icon}
|
||||
iconComponent={(privacyItems[value] || {}).iconComponent}
|
||||
items={items}
|
||||
onChange={onChange}
|
||||
isUserTouching={isUserTouching}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={formatMessage(messages.change_privacy)}
|
||||
container={container}
|
||||
value={value}
|
||||
/>
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
iconComponent={valueOption.iconComponent}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Overlay show={open} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<PrivacyDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
publish: {
|
||||
defaultMessage: 'Publish',
|
||||
id: 'compose_form.publish',
|
||||
},
|
||||
publishLoud: {
|
||||
defaultMessage: '{publish}!',
|
||||
id: 'compose_form.publish_loud',
|
||||
},
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
private: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
||||
direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
});
|
||||
|
||||
class Publisher extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onSecondarySubmit: PropTypes.func,
|
||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
|
||||
|
||||
const privacyIcons = {
|
||||
direct: {
|
||||
id: 'envelope',
|
||||
icon: MailIcon,
|
||||
},
|
||||
private: {
|
||||
id: 'lock',
|
||||
icon: LockIcon,
|
||||
},
|
||||
public: {
|
||||
id: 'globe',
|
||||
icon: PublicIcon,
|
||||
},
|
||||
unlisted: {
|
||||
id: 'unlock',
|
||||
icon: LockOpenIcon,
|
||||
},
|
||||
};
|
||||
|
||||
let publishText;
|
||||
if (isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (privacy === 'private' || privacy === 'direct') {
|
||||
const icon = privacyIcons[privacy];
|
||||
publishText = (
|
||||
<span>
|
||||
<Icon {...icon} /> {intl.formatMessage(messages.publish)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
const privacyNames = {
|
||||
public: messages.public,
|
||||
unlisted: messages.unlisted,
|
||||
private: messages.private,
|
||||
direct: messages.direct,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='compose-form__publish'>
|
||||
{sideArm && !isEditing && sideArm !== 'none' && (
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
className='side_arm'
|
||||
disabled={disabled}
|
||||
onClick={onSecondarySubmit}
|
||||
style={{ padding: null }}
|
||||
text={<Icon {...privacyIcons[sideArm]} />}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[sideArm])}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
className='primary'
|
||||
type='submit'
|
||||
text={publishText}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[privacy])}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(Publisher);
|
|
@ -5,7 +5,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||
|
@ -52,9 +51,9 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
<DisplayName account={status.get('account')} inline />
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { withRouter } from 'react-router-dom';
|
|||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
|
@ -186,9 +185,9 @@ class Search extends PureComponent {
|
|||
};
|
||||
|
||||
handleURLClick = () => {
|
||||
const { onOpenURL, history } = this.props;
|
||||
const { value, onOpenURL, history } = this.props;
|
||||
|
||||
onOpenURL(history);
|
||||
onOpenURL(value, history);
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
|
@ -331,7 +330,7 @@ class Search extends PureComponent {
|
|||
type='text'
|
||||
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
||||
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
||||
value={value || ''}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onFocus={this.handleFocus}
|
||||
|
@ -339,8 +338,8 @@ class Search extends PureComponent {
|
|||
/>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' icon={CancelIcon} className={hasValue ? 'active' : ''} />
|
||||
<Icon id='search' icon={SearchIcon} className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' icon={CancelIcon} className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
|
||||
<div className='search__popout'>
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Icon } from 'flavours/glitch/components/icon';
|
|||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||
import { SearchSection } from 'flavours/glitch/features/explore/components/search_section';
|
||||
|
||||
|
||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
|
@ -77,10 +76,10 @@ class SearchResults extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<header className='search-results__header'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' icon={SearchIcon} />
|
||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
{hashtags}
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import ForumIcon from '@/material-icons/400-24px/forum.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
localOnly: {
|
||||
defaultMessage: 'This post is local-only',
|
||||
id: 'advanced_options.local-only.tooltip',
|
||||
},
|
||||
threadedMode: {
|
||||
defaultMessage: 'Threaded mode enabled',
|
||||
id: 'advanced_options.threaded_mode.tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
// We use an array of tuples here instead of an object because it
|
||||
// preserves order.
|
||||
const iconMap = [
|
||||
['do_not_federate', 'home', HomeIcon, messages.localOnly],
|
||||
['threaded_mode', 'comments', ForumIcon, messages.threadedMode],
|
||||
];
|
||||
|
||||
class TextareaIcons extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { advancedOptions, intl } = this.props;
|
||||
return (
|
||||
<div className='compose-form__textarea-icons'>
|
||||
{advancedOptions && iconMap.map(
|
||||
([key, icon, iconComponent, message]) => advancedOptions.get(key) && (
|
||||
<span
|
||||
className='textarea_icon'
|
||||
key={key}
|
||||
title={intl.formatMessage(message)}
|
||||
>
|
||||
<Icon id={icon} icon={iconComponent} />
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(TextareaIcons);
|
|
@ -12,7 +12,6 @@ import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
|||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
export default class Upload extends ImmutablePureComponent {
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => ({
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
};
|
||||
|
||||
class UploadButton extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
onSelectFile: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
resetFileKey: PropTypes.number,
|
||||
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
this.props.onSelectFile(e.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.fileElement.click();
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.fileElement = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = intl.formatMessage(messages.upload);
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='paperclip' iconComponent={AddPhotoAlternateIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{message}</span>
|
||||
<input
|
||||
key={resetFileKey}
|
||||
ref={this.setRef}
|
||||
type='file'
|
||||
multiple
|
||||
accept={acceptContentTypes.toArray().join(',')}
|
||||
onChange={this.handleChange}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(UploadButton));
|
|
@ -1,9 +1,5 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
|
||||
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
|
@ -11,53 +7,15 @@ import {
|
|||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeSpoilerness,
|
||||
changeComposeVisibility,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from '../../../actions/compose';
|
||||
import { changeLocalSetting } from '../../../actions/local_settings';
|
||||
import {
|
||||
openModal,
|
||||
} from '../../../actions/modal';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
missingDescriptionMessage: {
|
||||
id: 'confirmations.missing_media_description.message',
|
||||
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.',
|
||||
},
|
||||
missingDescriptionConfirm: {
|
||||
id: 'confirmations.missing_media_description.confirm',
|
||||
defaultMessage: 'Send anyway',
|
||||
},
|
||||
missingDescriptionEdit: {
|
||||
id: 'confirmations.missing_media_description.edit',
|
||||
defaultMessage: 'Edit media',
|
||||
},
|
||||
});
|
||||
|
||||
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']);
|
||||
const sideArmRestrictedPrivacy = replyPrivacy ? privacyPreference(replyPrivacy, sideArmBasePrivacy) : null;
|
||||
let sideArmPrivacy = null;
|
||||
switch (state.getIn(['local_settings', 'side_arm_reply_mode'])) {
|
||||
case 'copy':
|
||||
sideArmPrivacy = replyPrivacy;
|
||||
break;
|
||||
case 'restrict':
|
||||
sideArmPrivacy = sideArmRestrictedPrivacy;
|
||||
break;
|
||||
}
|
||||
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']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoilerText: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
|
@ -70,17 +28,9 @@ const mapStateToProps = state => ({
|
|||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
advancedOptions: state.getIn(['compose', 'advanced_options']),
|
||||
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']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onChange (text) {
|
||||
dispatch(changeCompose(text));
|
||||
|
@ -102,48 +52,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(selectComposeSuggestion(position, token, suggestion, path));
|
||||
},
|
||||
|
||||
onChangeSpoilerText (text) {
|
||||
dispatch(changeComposeSpoilerText(text));
|
||||
onChangeSpoilerText (checked) {
|
||||
dispatch(changeComposeSpoilerText(checked));
|
||||
},
|
||||
|
||||
onPaste (files) {
|
||||
dispatch(uploadCompose(files));
|
||||
},
|
||||
|
||||
onPickEmoji (position, emoji) {
|
||||
dispatch(insertEmojiCompose(position, emoji));
|
||||
},
|
||||
|
||||
onChangeSpoilerness() {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
},
|
||||
|
||||
onChangeVisibility(value) {
|
||||
dispatch(changeComposeVisibility(value));
|
||||
},
|
||||
|
||||
onMediaDescriptionConfirm(routerHistory, mediaId, overriddenVisibility = null) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.missingDescriptionMessage),
|
||||
confirm: intl.formatMessage(messages.missingDescriptionConfirm),
|
||||
onConfirm: () => {
|
||||
if (overriddenVisibility) {
|
||||
dispatch(changeComposeVisibility(overriddenVisibility));
|
||||
}
|
||||
dispatch(submitCompose(routerHistory));
|
||||
},
|
||||
secondary: intl.formatMessage(messages.missingDescriptionEdit),
|
||||
onSecondary: () => dispatch(openModal({
|
||||
modalType: 'FOCAL_POINT',
|
||||
modalProps: { id: mediaId },
|
||||
})),
|
||||
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
|
||||
},
|
||||
}));
|
||||
onPickEmoji (position, data, needsSpace) {
|
||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
|
||||
import { isUserTouching } from 'flavours/glitch/is_mobile';
|
||||
|
||||
import Dropdown from '../components/dropdown';
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal({ modalType: 'ACTIONS', modalProps: props })),
|
||||
onModalClose: () => dispatch(closeModal({ modalType: undefined, ignoreFocus: false })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Dropdown);
|
|
@ -1,42 +0,0 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
||||
|
||||
import Header from '../components/header';
|
||||
|
||||
const messages = defineMessages({
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||
showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onSettingsClick (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(openModal({ modalType: 'SETTINGS', modalProps: {} }));
|
||||
},
|
||||
onLogout () {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header));
|
|
@ -1,56 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
changeComposeAdvancedOption,
|
||||
changeComposeContentType,
|
||||
addPoll,
|
||||
removePoll,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
import Options from '../components/options';
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const poll = state.getIn(['compose', 'poll']);
|
||||
const media = state.getIn(['compose', 'media_attachments']);
|
||||
const pending_media = state.getIn(['compose', 'pending_media_attachments']);
|
||||
return {
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
hasPoll: !!poll,
|
||||
allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4),
|
||||
allowPoll: !(media && !!media.size),
|
||||
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
||||
contentType: state.getIn(['compose', 'content_type']),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onChangeAdvancedOption(option, value) {
|
||||
dispatch(changeComposeAdvancedOption(option, value));
|
||||
},
|
||||
|
||||
onChangeContentType(value) {
|
||||
dispatch(changeComposeContentType(value));
|
||||
},
|
||||
|
||||
onTogglePoll() {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(removePoll());
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onDoodleOpen() {
|
||||
dispatch(openModal({
|
||||
modalType: 'DOODLE',
|
||||
modalProps: { noEsc: true, noClose: true },
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Options);
|
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { addPoll, removePoll } from '../../../actions/compose';
|
||||
import PollButton from '../components/poll_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(removePoll());
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollButton);
|
|
@ -42,8 +42,8 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(showSearch());
|
||||
},
|
||||
|
||||
onOpenURL (routerHistory) {
|
||||
dispatch(openURL(routerHistory));
|
||||
onOpenURL (q, routerHistory) {
|
||||
dispatch(openURL(q, routerHistory));
|
||||
},
|
||||
|
||||
onClickSearchResult (q, type) {
|
||||
|
|
|
@ -20,15 +20,11 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
|
||||
const spoilerText = state.getIn(['compose', 'spoiler_text']);
|
||||
return {
|
||||
active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
mediaCount: state.getIn(['compose', 'media_attachments']).size,
|
||||
};
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
mediaCount: state.getIn(['compose', 'media_attachments']).size,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
import TextIconButton from '../components/text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
||||
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
label: 'CW',
|
||||
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
|
||||
active: state.getIn(['compose', 'spoiler']),
|
||||
ariaControls: 'cw-spoiler-input',
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
unavailable: state.getIn(['compose', 'poll']) !== null,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onSelectFile (files) {
|
||||
dispatch(uploadCompose(files));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadButton);
|
|
@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import { profileLink, privacyPolicyLink } from 'flavours/glitch/utils/backend_links';
|
||||
import { HASHTAG_PATTERN_REGEX } from 'flavours/glitch/utils/hashtags';
|
||||
|
||||
import Warning from '../components/warning';
|
||||
|
@ -18,7 +17,7 @@ const mapStateToProps = state => ({
|
|||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
|
||||
if (hashtagWarning) {
|
||||
|
@ -28,7 +27,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
|||
if (directMessageWarning) {
|
||||
const message = (
|
||||
<span>
|
||||
<FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> {!!privacyPolicyLink && <a href={privacyPolicyLink} target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
|
||||
<FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -3,93 +3,138 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { mountCompose, unmountCompose, cycleElefriendCompose } from 'flavours/glitch/actions/compose';
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
||||
|
||||
import elephantUIPlane from '../../../../images/elephant_ui_plane.svg';
|
||||
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
|
||||
import { mascot } from '../../initial_state';
|
||||
import { isMobile } from '../../is_mobile';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import HeaderContainer from './containers/header_container';
|
||||
import NavigationContainer from './containers/navigation_container';
|
||||
import SearchContainer from './containers/search_container';
|
||||
import SearchResultsContainer from './containers/search_results_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
elefriend: state.getIn(['compose', 'elefriend']),
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClickElefriend () {
|
||||
dispatch(cycleElefriendCompose());
|
||||
},
|
||||
|
||||
onMount () {
|
||||
dispatch(mountCompose());
|
||||
},
|
||||
|
||||
onUnmount () {
|
||||
dispatch(unmountCompose());
|
||||
},
|
||||
});
|
||||
|
||||
class Compose extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
showSearch: PropTypes.bool,
|
||||
elefriend: PropTypes.number,
|
||||
onClickElefriend: PropTypes.func,
|
||||
onMount: PropTypes.func,
|
||||
onUnmount: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.props.onMount();
|
||||
const { dispatch } = this.props;
|
||||
dispatch(mountCompose());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.onUnmount();
|
||||
const { dispatch } = this.props;
|
||||
dispatch(unmountCompose());
|
||||
}
|
||||
|
||||
handleLogoutClick = e => {
|
||||
const { dispatch, intl } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.props.dispatch(changeComposing(true));
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.props.dispatch(changeComposing(false));
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
elefriend,
|
||||
intl,
|
||||
multiColumn,
|
||||
onClickElefriend,
|
||||
showSearch,
|
||||
} = this.props;
|
||||
const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
|
||||
const { multiColumn, showSearch, intl } = this.props;
|
||||
|
||||
if (multiColumn) {
|
||||
return (
|
||||
<div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
|
||||
<HeaderContainer />
|
||||
const { columns } = this.props;
|
||||
|
||||
{multiColumn && <SearchContainer />}
|
||||
return (
|
||||
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
|
||||
<nav className='drawer__header'>
|
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' icon={MenuIcon} /></Link>
|
||||
{!columns.some(column => column.get('id') === 'HOME') && (
|
||||
<Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' icon={HomeIcon} /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
|
||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' icon={NotificationsIcon} /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
|
||||
<Link to='/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' icon={PeopleIcon} /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
||||
<Link to='/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' icon={PublicIcon} /></Link>
|
||||
)}
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' icon={SettingsIcon} /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a>
|
||||
</nav>
|
||||
|
||||
{multiColumn && <SearchContainer /> }
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner'>
|
||||
<NavigationContainer />
|
||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
||||
|
||||
<div className='drawer__inner__mastodon'>
|
||||
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -106,8 +151,8 @@ class Compose extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<NavigationContainer />
|
||||
<Column onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer />
|
||||
|
||||
<Helmet>
|
||||
|
@ -119,4 +164,4 @@ class Compose extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Compose));
|
||||
export default connect(mapStateToProps)(injectIntl(Compose));
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_LANGUAGE_CHANGE,
|
||||
COMPOSE_COMPOSING_CHANGE,
|
||||
COMPOSE_CONTENT_TYPE_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
|
@ -87,9 +88,10 @@ const initialState = ImmutableMap({
|
|||
caretPosition: null,
|
||||
preselectDate: null,
|
||||
in_reply_to: null,
|
||||
is_composing: false,
|
||||
is_submitting: false,
|
||||
is_uploading: false,
|
||||
is_changing_upload: false,
|
||||
is_uploading: false,
|
||||
progress: 0,
|
||||
isUploadingThumbnail: false,
|
||||
thumbnailProgress: 0,
|
||||
|
@ -252,7 +254,7 @@ function removeMedia(state, mediaId) {
|
|||
|
||||
const insertSuggestion = (state, position, token, completion, path) => {
|
||||
return state.withMutations(map => {
|
||||
map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion}${completion[0] === ':' ? '\u200B' : ' '}${oldText.slice(position + token.length)}`);
|
||||
map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
|
||||
map.set('suggestion_token', null);
|
||||
map.set('suggestions', ImmutableList());
|
||||
if (path.length === 1 && path[0] === 'text') {
|
||||
|
@ -294,14 +296,15 @@ const sortHashtagsByUse = (state, tags) => {
|
|||
return sorted;
|
||||
};
|
||||
|
||||
const insertEmoji = (state, position, emojiData) => {
|
||||
const emoji = emojiData.native;
|
||||
const insertEmoji = (state, position, emojiData, needsSpace) => {
|
||||
const oldText = state.get('text');
|
||||
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
|
||||
|
||||
return state.withMutations(map => {
|
||||
map.update('text', oldText => `${oldText.slice(0, position)}${emoji}\u200B${oldText.slice(position)}`);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', position + emoji.length + 1);
|
||||
map.set('idempotencyKey', uuid());
|
||||
return state.merge({
|
||||
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
|
||||
focusDate: new Date(),
|
||||
caretPosition: position + emoji.length + 1,
|
||||
idempotencyKey: uuid(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -370,7 +373,9 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_MOUNT:
|
||||
return state.set('mounted', state.get('mounted') + 1);
|
||||
case COMPOSE_UNMOUNT:
|
||||
return state.set('mounted', Math.max(state.get('mounted') - 1, 0));
|
||||
return state
|
||||
.set('mounted', Math.max(state.get('mounted') - 1, 0))
|
||||
.set('is_composing', false);
|
||||
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
|
||||
return state
|
||||
.set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
|
||||
|
@ -408,6 +413,8 @@ export default function compose(state = initialState, action) {
|
|||
return state
|
||||
.set('text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_COMPOSING_CHANGE:
|
||||
return state.set('is_composing', action.value);
|
||||
case COMPOSE_CYCLE_ELEFRIEND:
|
||||
return state
|
||||
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
|
||||
|
@ -553,7 +560,7 @@ export default function compose(state = initialState, action) {
|
|||
return state;
|
||||
}
|
||||
case COMPOSE_EMOJI_INSERT:
|
||||
return insertEmoji(state, action.position, action.emoji);
|
||||
return insertEmoji(state, action.position, action.emoji, action.needsSpace);
|
||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return state
|
||||
.set('is_changing_upload', false)
|
||||
|
|
|
@ -648,6 +648,11 @@ body > [data-popper-placement] {
|
|||
padding: 10px;
|
||||
font-family: inherit;
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
|
@ -733,25 +738,9 @@ body > [data-popper-placement] {
|
|||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
column-gap: 5px;
|
||||
|
||||
.compose-form__publish-button-wrapper {
|
||||
padding-top: 10px;
|
||||
|
||||
button {
|
||||
padding: 7px 10px;
|
||||
text-align: center;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.side_arm {
|
||||
height: 100%;
|
||||
}
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -774,28 +763,6 @@ body > [data-popper-placement] {
|
|||
opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.compose-form__textarea-icons {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
inset-inline-end: 5px;
|
||||
bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .textarea_icon {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-inline-start: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: $lighter-text-color;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.sign-in-banner {
|
||||
padding: 10px;
|
||||
|
||||
|
@ -858,6 +825,7 @@ body > [data-popper-placement] {
|
|||
max-width: 100%;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
padding-inline-end: 25px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue