mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-07 18:31:07 +00:00
Composer Quote UI (#35805)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
parent
28bf811a07
commit
d4b2e7f771
|
@ -53,6 +53,7 @@ const AutosuggestTextarea = forwardRef(({
|
||||||
onFocus,
|
onFocus,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
lang,
|
lang,
|
||||||
|
className,
|
||||||
}, textareaRef) => {
|
}, textareaRef) => {
|
||||||
|
|
||||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
|
@ -192,7 +193,7 @@ const AutosuggestTextarea = forwardRef(({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-textarea'>
|
<div className={classNames('autosuggest-textarea', className)}>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
|
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||||
|
@ -34,6 +35,8 @@ import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import { StatusThreadLabel } from './status_thread_label';
|
import { StatusThreadLabel } from './status_thread_label';
|
||||||
import { VisibilityIcon } from './visibility_icon';
|
import { VisibilityIcon } from './visibility_icon';
|
||||||
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||||
|
@ -75,6 +78,7 @@ const messages = defineMessages({
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
|
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
@ -126,6 +130,7 @@ class Status extends ImmutablePureComponent {
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
|
contextType: PropTypes.string,
|
||||||
...WithOptionalRouterPropTypes,
|
...WithOptionalRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -359,6 +364,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuoteCancel = () => {
|
||||||
|
this.props.onQuoteCancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
_properStatus () {
|
_properStatus () {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
|
@ -573,6 +582,16 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{this.props.contextType === 'compose' && isQuotedPost && (
|
||||||
|
<IconButton
|
||||||
|
onClick={this.handleQuoteCancel}
|
||||||
|
className='status__quote-cancel'
|
||||||
|
title={intl.formatMessage(messages.quote_cancel)}
|
||||||
|
icon="cancel-fill"
|
||||||
|
iconComponent={CancelFillIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import { deleteModal } from '../initial_state';
|
import { deleteModal } from '../initial_state';
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||||
|
import { quoteComposeCancel } from '../actions/compose_typed';
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
@ -111,6 +112,12 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onQuoteCancel() {
|
||||||
|
if (contextType === 'compose') {
|
||||||
|
dispatch(quoteComposeCancel());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onRevokeQuote (status) {
|
onRevokeQuote (status) {
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { PollForm } from "./poll_form";
|
||||||
import { ReplyIndicator } from './reply_indicator';
|
import { ReplyIndicator } from './reply_indicator';
|
||||||
import { UploadForm } from './upload_form';
|
import { UploadForm } from './upload_form';
|
||||||
import { Warning } from './warning';
|
import { Warning } from './warning';
|
||||||
|
import { ComposeQuotedStatus } from './quoted_post';
|
||||||
|
|
||||||
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 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';
|
||||||
|
|
||||||
|
@ -304,10 +305,12 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
lang={this.props.lang}
|
lang={this.props.lang}
|
||||||
|
className='compose-form__input'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadForm />
|
<UploadForm />
|
||||||
<PollForm />
|
<PollForm />
|
||||||
|
<ComposeQuotedStatus />
|
||||||
|
|
||||||
<div className='compose-form__footer'>
|
<div className='compose-form__footer'>
|
||||||
<div className='compose-form__actions'>
|
<div className='compose-form__actions'>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
|
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
||||||
|
import { useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
|
export const ComposeQuotedStatus: FC = () => {
|
||||||
|
const quotedStatusId = useAppSelector(
|
||||||
|
(state) => state.compose.get('quoted_status_id') as string | null,
|
||||||
|
);
|
||||||
|
const quote = useMemo(
|
||||||
|
() =>
|
||||||
|
quotedStatusId
|
||||||
|
? Map<'state' | 'quoted_status', string>([
|
||||||
|
['state', 'accepted'],
|
||||||
|
['quoted_status', quotedStatusId],
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
[quotedStatusId],
|
||||||
|
);
|
||||||
|
if (!quote) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <QuotedStatus quote={quote} contextType='compose' />;
|
||||||
|
};
|
|
@ -3,10 +3,16 @@ import { connect } from 'react-redux';
|
||||||
import { addPoll, removePoll } from '../../../actions/compose';
|
import { addPoll, removePoll } from '../../../actions/compose';
|
||||||
import PollButton from '../components/poll_button';
|
import PollButton from '../components/poll_button';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => {
|
||||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
const readyAttachmentsSize = state.compose.get('media_attachments').size ?? 0;
|
||||||
|
const hasAttachments = readyAttachmentsSize > 0 || !!state.compose.get('is_uploading');
|
||||||
|
const hasQuote = !!state.compose.get('quoted_status_id');
|
||||||
|
|
||||||
|
return ({
|
||||||
|
disabled: hasAttachments || hasQuote,
|
||||||
active: state.getIn(['compose', 'poll']) !== null,
|
active: state.getIn(['compose', 'poll']) !== null,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ const mapStateToProps = state => {
|
||||||
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
||||||
const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments'])-1;
|
const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments'])-1;
|
||||||
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
||||||
|
const hasQuote = !!state.compose.get('quoted_status_id');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio,
|
disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio || hasQuote,
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -880,6 +880,7 @@
|
||||||
"status.mute_conversation": "Mute conversation",
|
"status.mute_conversation": "Mute conversation",
|
||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
|
"status.quote.cancel": "Cancel quote",
|
||||||
"status.quote_error.filtered": "Hidden due to one of your filters",
|
"status.quote_error.filtered": "Hidden due to one of your filters",
|
||||||
"status.quote_error.not_available": "Post unavailable",
|
"status.quote_error.not_available": "Post unavailable",
|
||||||
"status.quote_error.pending_approval": "Post pending",
|
"status.quote_error.pending_approval": "Post pending",
|
||||||
|
|
|
@ -331,8 +331,16 @@ export const composeReducer = (state = initialState, action) => {
|
||||||
return state.set('is_changing_upload', false);
|
return state.set('is_changing_upload', false);
|
||||||
} else if (quoteComposeByStatus.match(action)) {
|
} else if (quoteComposeByStatus.match(action)) {
|
||||||
const status = action.payload;
|
const status = action.payload;
|
||||||
if (status.getIn(['quote_approval', 'current_user']) === 'automatic') {
|
if (
|
||||||
return state.set('quoted_status_id', status.get('id'));
|
status.getIn(['quote_approval', 'current_user']) === 'automatic' &&
|
||||||
|
state.get('media_attachments').size === 0 &&
|
||||||
|
!state.get('is_uploading') &&
|
||||||
|
!state.get('poll')
|
||||||
|
) {
|
||||||
|
return state
|
||||||
|
.set('quoted_status_id', status.get('id'))
|
||||||
|
.set('spoiler', status.get('sensitive'))
|
||||||
|
.set('spoiler_text', status.get('spoiler_text'));
|
||||||
}
|
}
|
||||||
} else if (quoteComposeCancel.match(action)) {
|
} else if (quoteComposeCancel.match(action)) {
|
||||||
return state.set('quoted_status_id', null);
|
return state.set('quoted_status_id', null);
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const getFilters = createSelector(
|
||||||
(_, { contextType }: { contextType: string }) => contextType,
|
(_, { contextType }: { contextType: string }) => contextType,
|
||||||
],
|
],
|
||||||
(filters, contextType) => {
|
(filters, contextType) => {
|
||||||
if (!contextType) {
|
if (!contextType || contextType === 'compose') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ export function createAppThunk<Arg = void, Returned = void, ExtraArg = unknown>(
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Object.assign({}, action, actionCreator);
|
return Object.assign(actionCreator, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBaseAsyncThunk = rtkCreateAsyncThunk.withTypes<AppThunkConfig>();
|
const createBaseAsyncThunk = rtkCreateAsyncThunk.withTypes<AppThunkConfig>();
|
||||||
|
|
|
@ -946,6 +946,24 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status__quote {
|
||||||
|
margin: 0 8px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// Override .status__content .status__content__text.status__content__text--visible
|
||||||
|
.status__content__text.status__content__text {
|
||||||
|
display: -webkit-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__content__text {
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-button {
|
.dropdown-button {
|
||||||
|
@ -1583,6 +1601,7 @@ body > [data-popper-placement] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
.display-name {
|
.display-name {
|
||||||
bdi {
|
bdi {
|
||||||
|
@ -1599,6 +1618,11 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status__quote-cancel {
|
||||||
|
align-self: self-start;
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.status__info {
|
.status__info {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user