[Glitch] Composer quote improvements

Port f85f0eee1b to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2025-08-21 16:07:31 +02:00
committed by Claire
parent 5cac4dfb5c
commit be78b64948
14 changed files with 363 additions and 96 deletions

View File

@@ -15,10 +15,8 @@ import { missingAltTextModal } from 'flavours/glitch/initial_state';
import AutosuggestInput from 'flavours/glitch/components/autosuggest_input';
import AutosuggestTextarea from 'flavours/glitch/components/autosuggest_textarea';
import { Button } from 'flavours/glitch/components/button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import { countableText } from '../util/counter';
@@ -36,6 +34,7 @@ import { ThreadModeButton } from './thread_mode_button';
import { UploadForm } from './upload_form';
import { Warning } from './warning';
import { ComposeQuotedStatus } from './quoted_post';
import { VisibilityButton } from './visibility_button';
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';
@@ -280,7 +279,7 @@ class ComposeForm extends ImmutablePureComponent {
<EditIndicator />
<div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<VisibilityButton disabled={this.props.isEditing} />
<LanguageDropdown />
</div>

View File

@@ -1,15 +1,17 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import type { FC } from 'react';
import { Map } from 'immutable';
import { quoteComposeCancel } from '@/flavours/glitch/actions/compose_typed';
import { QuotedStatus } from '@/flavours/glitch/components/status_quoted';
import { useAppSelector } from '@/flavours/glitch/store';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
export const ComposeQuotedStatus: FC = () => {
const quotedStatusId = useAppSelector(
(state) => state.compose.get('quoted_status_id') as string | null,
);
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const quote = useMemo(
() =>
quotedStatusId
@@ -20,8 +22,17 @@ export const ComposeQuotedStatus: FC = () => {
: null,
[quotedStatusId],
);
const dispatch = useAppDispatch();
const handleQuoteCancel = useCallback(() => {
dispatch(quoteComposeCancel());
}, [dispatch]);
if (!quote) {
return null;
}
return <QuotedStatus quote={quote} contextType='compose' />;
return (
<QuotedStatus
quote={quote}
onQuoteCancel={!isEditing ? handleQuoteCancel : undefined}
/>
);
};

View File

@@ -0,0 +1,148 @@
import { useCallback, useMemo } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/flavours/glitch/actions/compose';
import { setComposeQuotePolicy } from '@/flavours/glitch/actions/compose_typed';
import { openModal } from '@/flavours/glitch/actions/modal';
import type { ApiQuotePolicy } from '@/flavours/glitch/api_types/quotes';
import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses';
import { Icon } from '@/flavours/glitch/components/icon';
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store';
import { isFeatureEnabled } from '@/flavours/glitch/utils/environment';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import { messages as privacyMessages } from './privacy_dropdown';
const messages = defineMessages({
anyone_quote: {
id: 'privacy.quote.anyone',
defaultMessage: '{visibility}, anyone can quote',
},
limited_quote: {
id: 'privacy.quote.limited',
defaultMessage: '{visibility}, quotes limited',
},
disabled_quote: {
id: 'privacy.quote.disabled',
defaultMessage: '{visibility}, quotes disabled',
},
});
interface PrivacyDropdownProps {
disabled?: boolean;
}
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
if (!isFeatureEnabled('outgoing_quotes')) {
return <PrivacyDropdownContainer {...props} />;
}
return <PrivacyModalButton {...props} />;
};
const visibilityOptions = {
public: {
icon: 'globe',
iconComponent: PublicIcon,
value: 'public',
text: privacyMessages.public_short,
},
unlisted: {
icon: 'unlock',
iconComponent: QuietTimeIcon,
value: 'unlisted',
text: privacyMessages.unlisted_short,
},
private: {
icon: 'lock',
iconComponent: LockIcon,
value: 'private',
text: privacyMessages.private_short,
},
direct: {
icon: 'at',
iconComponent: AlternateEmailIcon,
value: 'direct',
text: privacyMessages.direct_short,
},
};
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
const intl = useIntl();
const { visibility, quotePolicy } = useAppSelector((state) => ({
visibility: state.compose.get('privacy') as StatusVisibility,
quotePolicy: state.compose.get('quote_policy') as ApiQuotePolicy,
}));
const { icon, iconComponent } = useMemo(() => {
const option = visibilityOptions[visibility];
return { icon: option.icon, iconComponent: option.iconComponent };
}, [visibility]);
const text = useMemo(() => {
const visibilityText = intl.formatMessage(
visibilityOptions[visibility].text,
);
if (visibility === 'private' || visibility === 'direct') {
return visibilityText;
}
if (quotePolicy === 'nobody') {
return intl.formatMessage(messages.disabled_quote, {
visibility: visibilityText,
});
}
if (quotePolicy !== 'public') {
return intl.formatMessage(messages.limited_quote, {
visibility: visibilityText,
});
}
return intl.formatMessage(messages.anyone_quote, {
visibility: visibilityText,
});
}, [quotePolicy, visibility, intl]);
const dispatch = useAppDispatch();
const handleChange: VisibilityModalCallback = useCallback(
(newVisibility, newQuotePolicy) => {
if (newVisibility !== visibility) {
dispatch(changeComposeVisibility(newVisibility));
}
if (newQuotePolicy !== quotePolicy) {
dispatch(setComposeQuotePolicy(newQuotePolicy));
}
},
[dispatch, quotePolicy, visibility],
);
const handleOpen = useCallback(() => {
dispatch(
openModal({
modalType: 'COMPOSE_PRIVACY',
modalProps: { onChange: handleChange },
}),
);
}, [dispatch, handleChange]);
return (
<button
type='button'
title={intl.formatMessage(privacyMessages.change_privacy)}
onClick={handleOpen}
disabled={disabled}
className={classNames('dropdown-button')}
>
<Icon id={icon} icon={iconComponent} />
<span className='dropdown-button__label'>{text}</span>
</button>
);
};