From 757d8c26583209048485e68f9a87fbe20cb521c7 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 11 Sep 2026 12:30:34 +0000 Subject: [PATCH] Composer redesign: Verified autocomplete, track last quote policy, editing discard warning (#40461) --- .../mastodon/components/autosuggest/items.tsx | 16 ++++- .../components/autosuggest/styles.module.scss | 11 ++++ .../compose/redesign/modal_cancel.tsx | 64 ++++++++++++++----- .../compose/redesign/styles.module.scss | 18 +++++- .../features/compose/redesign/visibility.tsx | 15 +++-- app/javascript/mastodon/locales/en.json | 4 ++ app/javascript/mastodon/utils/links.ts | 9 +++ 7 files changed, 112 insertions(+), 25 deletions(-) diff --git a/app/javascript/mastodon/components/autosuggest/items.tsx b/app/javascript/mastodon/components/autosuggest/items.tsx index 1e293f0454d..ffe286b10c1 100644 --- a/app/javascript/mastodon/components/autosuggest/items.tsx +++ b/app/javascript/mastodon/components/autosuggest/items.tsx @@ -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 ( <> @@ -61,7 +68,14 @@ const AutosuggestAccount: React.FC<{ id: string }> = ({ id }) => { account={account} variant='noDomain' className={classes.itemAccountName} - /> + > + {verifiedField?.value_plain && ( + + + {urlToDomain(verifiedField.value_plain)} + + )} + {account.acct} diff --git a/app/javascript/mastodon/components/autosuggest/styles.module.scss b/app/javascript/mastodon/components/autosuggest/styles.module.scss index f70c3d0f7e0..cd3553caf31 100644 --- a/app/javascript/mastodon/components/autosuggest/styles.module.scss +++ b/app/javascript/mastodon/components/autosuggest/styles.module.scss @@ -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; +} diff --git a/app/javascript/mastodon/features/compose/redesign/modal_cancel.tsx b/app/javascript/mastodon/features/compose/redesign/modal_cancel.tsx index 0fa0eb7b0a1..88acdd2a7ab 100644 --- a/app/javascript/mastodon/features/compose/redesign/modal_cancel.tsx +++ b/app/javascript/mastodon/features/compose/redesign/modal_cancel.tsx @@ -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 ( - + {isEditing ? ( + + ) : ( + + )} - + {isEditing ? ( + + ) : ( + + )} diff --git a/app/javascript/mastodon/features/compose/redesign/styles.module.scss b/app/javascript/mastodon/features/compose/redesign/styles.module.scss index 7bfefa108b4..1082406c02e 100644 --- a/app/javascript/mastodon/features/compose/redesign/styles.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/styles.module.scss @@ -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; diff --git a/app/javascript/mastodon/features/compose/redesign/visibility.tsx b/app/javascript/mastodon/features/compose/redesign/visibility.tsx index 20e7e8c265d..c97cf21b47a 100644 --- a/app/javascript/mastodon/features/compose/redesign/visibility.tsx +++ b/app/javascript/mastodon/features/compose/redesign/visibility.tsx @@ -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 = diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 252aabf865c..5f15c01a5a3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/utils/links.ts b/app/javascript/mastodon/utils/links.ts index 00821fa8d14..b1c53af6b33 100644 --- a/app/javascript/mastodon/utils/links.ts +++ b/app/javascript/mastodon/utils/links.ts @@ -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 (