From 9830f6d361dcc8c0c767c79d66cbef6793cb3071 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 30 Jul 2026 14:26:42 +0200 Subject: [PATCH] Compose side fixes (#40000) --- .../mastodon/api_types/media_attachments.ts | 12 ++-- .../mastodon/components/autosuggest/utils.ts | 6 +- .../components/autosuggest_textarea.jsx | 4 +- .../mastodon/components/popover/index.tsx | 13 +++-- .../compose/components/language_dropdown.tsx | 10 ++-- .../mastodon/reducers/media_attachments.js | 4 +- app/javascript/mastodon/selectors/statuses.ts | 2 +- app/javascript/testing/api.ts | 56 ++++++++++++++++++- 8 files changed, 86 insertions(+), 21 deletions(-) diff --git a/app/javascript/mastodon/api_types/media_attachments.ts b/app/javascript/mastodon/api_types/media_attachments.ts index 6242569b82c..8cb798f7604 100644 --- a/app/javascript/mastodon/api_types/media_attachments.ts +++ b/app/javascript/mastodon/api_types/media_attachments.ts @@ -24,6 +24,7 @@ export interface ApiImageAttachmentJSON extends BaseApiMediaAttachmentJSON { meta: { original: ApiImageAttachmentMetaJSON; small: ApiImageAttachmentMetaJSON; + focus?: ApiFocusAttachmentMetaJSON; }; } @@ -42,10 +43,7 @@ export interface ApiVideoAttachmentJSON extends BaseApiMediaAttachmentJSON { colors: ApiColorsAttachmentMetaJSON; original: ApiVideoAttachmentMetaJSON; small: ApiImageAttachmentMetaJSON; - focus: { - x: number; - y: number; - }; + focus?: ApiFocusAttachmentMetaJSON; }; } @@ -54,6 +52,7 @@ export interface ApiGifvAttachmentJSON extends BaseApiMediaAttachmentJSON { meta: { original: ApiVideoAttachmentMetaJSON; small: ApiImageAttachmentMetaJSON; + focus?: ApiFocusAttachmentMetaJSON; }; } @@ -89,3 +88,8 @@ export interface ApiColorsAttachmentMetaJSON { foreground: string; accent: string; } + +export interface ApiFocusAttachmentMetaJSON { + x: number; + y: number; +} diff --git a/app/javascript/mastodon/components/autosuggest/utils.ts b/app/javascript/mastodon/components/autosuggest/utils.ts index 3e02114a97d..99a433358fb 100644 --- a/app/javascript/mastodon/components/autosuggest/utils.ts +++ b/app/javascript/mastodon/components/autosuggest/utils.ts @@ -23,12 +23,12 @@ export const textAtCursorMatchesToken = ( word = word.trim(); if (word.length < 3 || (word[0] && !searchTokens.includes(word[0]))) { - return [null, null]; + return [null, null] as const; } if (word.length > 0) { - return [left + 1, word]; + return [left + 1, word] as const; } else { - return [null, null]; + return [null, null] as const; } }; diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index ddf670c4cf2..3e48d38b6c2 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -153,7 +153,7 @@ const AutosuggestTextarea = forwardRef(({ } }, [lang]); - const renderSuggestion = (suggestion, i) => { + const renderSuggestion = useCallback((suggestion, i) => { let inner, key; if (suggestion.type === 'emoji') { @@ -172,7 +172,7 @@ const AutosuggestTextarea = forwardRef(({ {inner} ); - }; + }, [selectedSuggestion, handleSuggestionClick]); const handleRef = useCallback((element) => { textareaRef.current = element; diff --git a/app/javascript/mastodon/components/popover/index.tsx b/app/javascript/mastodon/components/popover/index.tsx index 5d7f84938d7..dd7a8dfabf5 100644 --- a/app/javascript/mastodon/components/popover/index.tsx +++ b/app/javascript/mastodon/components/popover/index.tsx @@ -95,13 +95,18 @@ export interface PopoverProps { * enable it to be sized and positioned. The `ref` prop * is not passed when the `popoverElement` prop is provided. */ - props: Record & { - ref?: React.RefCallback; - style: React.CSSProperties; - }; + props: PopoverChildProps; }) => React.ReactNode; } +export interface PopoverChildProps { + ref?: React.RefCallback; + style: React.CSSProperties; + 'data-popover-placement': Placement; + 'data-popover-reference-hidden'?: boolean; + 'data-popover-escaped'?: boolean; +} + export const Popover: React.FC = ({ isOpen, onClose, diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx index ebf577c0587..2a07675a505 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx @@ -56,13 +56,15 @@ const getFrequentlyUsedLanguages = createSelector( const isTextLongEnoughForGuess = (text: string) => text.length > 20; -const LanguageDropdownMenu: React.FC<{ +const emptyArray: Language[] = []; + +export const LanguageDropdownMenu: React.FC<{ value: string; - guess?: string; + guess: string; onClose: () => void; onChange: (arg0: string) => void; }> = ({ value, guess, onClose, onChange }) => { - const languages = preloadedLanguages as Language[]; + const languages = preloadedLanguages ?? emptyArray; const intl = useIntl(); const [searchValue, setSearchValue] = useState(''); const nodeRef = useRef(null); @@ -317,7 +319,7 @@ export const LanguageDropdown: React.FC = () => { const text = useAppSelector((state) => state.compose.get('text') as string); const current = - (preloadedLanguages as Language[]).find((lang) => lang[0] === value) ?? []; + (preloadedLanguages ?? []).find((lang) => lang[0] === value) ?? []; const handleMouseDown = useCallback(() => { if (!open && document.activeElement instanceof HTMLElement) { diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index cbb4933bc7e..650d13d9243 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,9 +1,9 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { STORE_HYDRATE } from '../actions/store'; const initialState = ImmutableMap({ - accept_content_types: [], + accept_content_types: ImmutableList(), }); export default function meta(state = initialState, action) { diff --git a/app/javascript/mastodon/selectors/statuses.ts b/app/javascript/mastodon/selectors/statuses.ts index 00ac0f3dd98..bbaf205f856 100644 --- a/app/javascript/mastodon/selectors/statuses.ts +++ b/app/javascript/mastodon/selectors/statuses.ts @@ -30,7 +30,7 @@ export const selectPlainStatus = createAppSelector( export const selectAccountStatus = createAppSelector( [ selectPlainStatus, - (state, statusId: string) => { + (state, statusId?: string | null) => { const accountId = state.statuses.getIn([statusId, 'account']); if (typeof accountId !== 'string') { return null; diff --git a/app/javascript/testing/api.ts b/app/javascript/testing/api.ts index ec75098d290..b8f9ccb3e04 100644 --- a/app/javascript/testing/api.ts +++ b/app/javascript/testing/api.ts @@ -2,9 +2,16 @@ import type { CompactEmoji } from 'emojibase'; import { http, HttpResponse } from 'msw'; import { action } from 'storybook/actions'; +import type { MediaAttachmentType } from '@/mastodon/api_types/media_attachments'; import { toSupportedLocale } from '@/mastodon/features/emoji/locale'; -import { customEmojiFactory, relationshipsFactoryAPI } from './factories'; +import { + customEmojiFactory, + mediaAttachmentFactoryAPI, + relationshipsFactoryAPI, +} from './factories'; + +const mediaStorageMap = new Map(); export const mockHandlers = { mute: http.post<{ id: string }>('/api/v1/accounts/:id/mute', ({ params }) => { @@ -43,6 +50,53 @@ export const mockHandlers = { ); }, ), + mediaUpload: http.post('/api/v2/media', async ({ request }) => { + action('uploaded media')(); + + const formData = await request.formData(); + const file = formData.get('file'); + if (!file) { + return new HttpResponse('Missing media', { status: 400 }); + } + + if (!(file instanceof File)) { + return new HttpResponse('Media is not file', { status: 400 }); + } + + const { type: mimeType } = file; + const id = mediaStorageMap.size.toString(); + mediaStorageMap.set(id, file); + let type: MediaAttachmentType = 'unknown'; + if (mimeType === 'image/gif') { + type = 'gifv'; + } else if (mimeType.startsWith('image/')) { + type = 'image'; + } else if (mimeType.startsWith('video/')) { + type = 'video'; + } else if (mimeType.startsWith('audio/')) { + type = 'audio'; + } + + return HttpResponse.json( + mediaAttachmentFactoryAPI({ + id, + type, + url: `/mock_media/${id}`, + preview_url: `/mock_media/${id}`, + }), + ); + }), + mediaGet: http.get<{ id: string }>('/mock_media/:id', async ({ params }) => { + const { id } = params; + action(`getting media id ${id}`)(); + + const media = mediaStorageMap.get(id); + if (!media) { + return new HttpResponse('Not found', { status: 404 }); + } + + return HttpResponse.arrayBuffer(await media.arrayBuffer()); + }), emojiCustomData: http.get('/api/v1/custom_emojis', () => { action('fetching custom emoji data')(); return HttpResponse.json([customEmojiFactory()]);