Composer redesign: Verified autocomplete, track last quote policy, editing discard warning (#40461)

This commit is contained in:
Echo
2026-09-11 12:30:34 +00:00
committed by GitHub
parent 12eb83b564
commit 757d8c2658
7 changed files with 112 additions and 25 deletions

View File

@@ -2,8 +2,11 @@ import type React from 'react';
import classNames from 'classnames';
import { SealCheckIcon } from '@phosphor-icons/react';
import { selectPlainAccount } from '@/mastodon/selectors/accounts';
import { useAppSelector } from '@/mastodon/store';
import { urlToDomain } from '@/mastodon/utils/links';
import { Avatar } from '../avatar';
import { DisplayName } from '../display_name';
@@ -53,6 +56,10 @@ const AutosuggestAccount: React.FC<{ id: string }> = ({ id }) => {
return null;
}
const verifiedField = account.fields.find(
(field) => field.verified_at !== null,
);
return (
<>
<Avatar account={account} className={classes.itemIcon} size={32} />
@@ -61,7 +68,14 @@ const AutosuggestAccount: React.FC<{ id: string }> = ({ id }) => {
account={account}
variant='noDomain'
className={classes.itemAccountName}
/>
>
{verifiedField?.value_plain && (
<span className={classes.itemAccountVerified}>
<SealCheckIcon weight='fill' />
{urlToDomain(verifiedField.value_plain)}
</span>
)}
</DisplayName>
<span className={classes.itemAccountHandle}>{account.acct}</span>
</div>
</>

View File

@@ -52,6 +52,10 @@
.itemAccountName {
@include mixins.type-label-lg;
display: flex;
align-items: stretch;
gap: var(--space-2xs);
}
.itemAccountHandle {
@@ -59,3 +63,10 @@
color: var(--color-text-tertiary);
}
.itemAccountVerified {
display: flex;
align-items: center;
color: var(--color-text-brand);
font-weight: normal;
}

View File

@@ -14,7 +14,7 @@ import {
requestComposerFocus,
resetComposer,
} from '@/mastodon/reducers/slices/composer';
import { useAppDispatch } from '@/mastodon/store';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
openNew,
@@ -37,32 +37,62 @@ const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
dispatch(requestComposerFocus());
}, [dispatch]);
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
return (
<ModalShell>
<ModalTitle>
<FormattedMessage
id='compose.cancel_modal.title'
defaultMessage='Discard draft'
/>
{isEditing ? (
<FormattedMessage
id='compose.cancel_modal.edit.title'
defaultMessage='Unsaved changes'
/>
) : (
<FormattedMessage
id='compose.cancel_modal.title'
defaultMessage='Discard draft'
/>
)}
</ModalTitle>
<FormattedMessage
id='compose.cancel_modal.body'
defaultMessage='You have a draft already in progress. What would you like to do?'
/>
{isEditing ? (
<FormattedMessage
id='compose.cancel_modal.edit.body'
defaultMessage='You were editing a post. What would you like to do?'
/>
) : (
<FormattedMessage
id='compose.cancel_modal.body'
defaultMessage='You have a draft already in progress. What would you like to do?'
/>
)}
<ModalActions>
<Button variant='solid' color='destructive' onClick={handleDelete}>
<FormattedMessage
id='compose.cancel_modal.delete'
defaultMessage='Delete draft'
/>
{isEditing ? (
<FormattedMessage
id='compose.cancel_modal.edit.delete'
defaultMessage='Discard changes'
/>
) : (
<FormattedMessage
id='compose.cancel_modal.delete'
defaultMessage='Delete draft'
/>
)}
</Button>
<Button variant='solid' onClick={handleContinue}>
<FormattedMessage
id='compose.cancel_modal.continue'
defaultMessage='Continue draft'
/>
{isEditing ? (
<FormattedMessage
id='compose.cancel_modal.edit.continue'
defaultMessage='Continue editing'
/>
) : (
<FormattedMessage
id='compose.cancel_modal.continue'
defaultMessage='Continue draft'
/>
)}
</Button>
</ModalActions>
</ModalShell>

View File

@@ -214,8 +214,22 @@ textarea.textarea {
// Reply
.reply {
border-inline-start: 0.5px solid var(--color-border-primary);
padding: 0 var(--space-md);
column-gap: var(--space-2xs);
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
&::before {
border-block-start: 2px solid var(--color-border-primary);
border-inline-start: 2px solid var(--color-border-primary);
border-top-left-radius: var(--radius-md);
box-sizing: border-box;
content: '';
display: block;
grid-row: span 2;
margin: var(--space-xs) var(--space-2xs) 0;
width: 20px;
}
a {
color: inherit;

View File

@@ -1,5 +1,4 @@
import type React from 'react';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -116,6 +115,11 @@ const ComposeVisibilityMenu: React.FC = () => {
const defaultQuotePolicy = useAppSelector(
(state) => state.compose.get('default_quote_policy') as ApiQuotePolicy,
);
// Track the last public quote policy, so the picker remembers what was last used before quoting was disabled.
const [lastQuotePolicy, setLastQuotePolicy] = useState(
defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public',
);
const quotePolicy = currentQuotePolicy ?? defaultQuotePolicy;
const isReply = useAppSelector((state) => !!state.compose.get('in_reply_to'));
@@ -146,22 +150,23 @@ const ComposeVisibilityMenu: React.FC = () => {
switch (value) {
case 'public':
newQuotePolicy = 'public';
setLastQuotePolicy(newQuotePolicy);
break;
case 'followers':
newQuotePolicy = 'followers';
setLastQuotePolicy(newQuotePolicy);
break;
case 'others':
// If it's not checked, then it's nobody.
if (checked) {
// Only use the default if it's not nobody, as then it'll never be enabled.
newQuotePolicy =
defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public';
newQuotePolicy = lastQuotePolicy;
}
break;
}
dispatch(setComposeQuotePolicy(newQuotePolicy));
},
[defaultQuotePolicy, dispatch],
[dispatch, lastQuotePolicy],
);
const handleSwitchToMessage: React.MouseEventHandler<HTMLButtonElement> =

View File

@@ -505,6 +505,10 @@
"compose.cancel_modal.body": "You have a draft already in progress. What would you like to do?",
"compose.cancel_modal.continue": "Continue draft",
"compose.cancel_modal.delete": "Delete draft",
"compose.cancel_modal.edit.body": "You were editing a post. What would you like to do?",
"compose.cancel_modal.edit.continue": "Continue editing",
"compose.cancel_modal.edit.delete": "Discard changes",
"compose.cancel_modal.edit.title": "Unsaved changes",
"compose.cancel_modal.title": "Discard draft",
"compose.counter": "{current, number}/{max, number}",
"compose.discoverable": "Discoverable in public feeds & search results",

View File

@@ -10,6 +10,15 @@ export function setupLinkListeners() {
on('click', ':not(form) button[data-confirm]:not([form])', handleConfirmLink);
}
export function urlToDomain(input: string | URL) {
try {
const url = new URL(input);
return url.hostname;
} catch {
return null;
}
}
function handleConfirmLink(event: MouseEvent) {
const target = event.currentTarget;
if (