mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-14 12:27:14 -05:00
Composer redesign: Verified autocomplete, track last quote policy, editing discard warning (#40461)
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user