Redesign composer thunks (#39945)

This commit is contained in:
Echo
2026-07-28 16:07:12 +02:00
committed by GitHub
parent 165613d2a6
commit cd909b22c0
12 changed files with 123 additions and 42 deletions

View File

@@ -3,24 +3,32 @@ import { defineMessages } from 'react-intl';
import { createAction } from '@reduxjs/toolkit';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { apiUpdateMedia } from 'mastodon/api/compose';
import { apiGetSearch } from 'mastodon/api/search';
import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { apiUpdateMedia } from '@/mastodon/api/compose';
import { apiGetSearch } from '@/mastodon/api/search';
import type { ApiMediaAttachmentJSON } from '@/mastodon/api_types/media_attachments';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { MediaAttachment } from '@/mastodon/models/media_attachment';
import type { Status, StatusVisibility } from '@/mastodon/models/status';
import type { RootState } from '@/mastodon/store';
import {
createDataLoadingThunk,
createAppThunk,
} from 'mastodon/store/typed_functions';
} from '@/mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status, StatusVisibility } from '../models/status';
import type { RootState } from '../store';
import type { ApiStatusJSON } from '../api_types/statuses';
import { showAlert } from './alerts';
import { changeCompose, focusCompose } from './compose';
import {
changeCompose,
focusCompose,
submitCompose as submitComposeApi,
uploadCompose,
} from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
const messages = defineMessages({
quoteErrorEdit: {
id: 'quote_error.edit',
@@ -159,13 +167,11 @@ export const quoteComposeByStatus = createAppThunk(
const state = getState();
const composeState = state.compose;
const mediaAttachments = composeState.get('media_attachments');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const wasQuietPostHintModalDismissed: boolean =
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
state.settings.getIn(
['dismissed_banners', 'quote/quiet_post_hint'],
false,
);
const wasQuietPostHintModalDismissed = !!state.settings.getIn(
['dismissed_banners', 'quote/quiet_post_hint'],
false,
);
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
@@ -278,3 +284,85 @@ export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
export const setDragUploadEnabled = createAction<boolean>(
'compose/setDragUploadEnabled',
);
export const submitCompose = createAppThunk(
(
{
textareaValue = '',
redirectOnSuccess,
}: { textareaValue?: string; redirectOnSuccess?: boolean },
{ getState, dispatch },
) => {
if (
textareaValue &&
(getState().compose.get('text') as string) !== textareaValue
) {
dispatch(changeCompose(textareaValue));
}
const { compose, meta, statuses, settings } = getState();
const privacy = compose.get('privacy') as StatusVisibility;
const missingAltText = (
compose.get('media_attachments') as unknown as Immutable.List<
Immutable.Map<string, string>
>
).some(
(media) =>
['image', 'gifv'].includes(media.get('type') ?? '') &&
(media.get('description') ?? '').length === 0,
);
const me = meta.get('me') as string | null;
const quotedStatusId = compose.get('quoted_status_id') as string | null;
const quoteToPrivate =
!!quotedStatusId &&
privacy === 'private' &&
statuses.getIn([quotedStatusId, 'account']) !== me &&
!settings.getIn(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID]);
if (
!!meta.get('missing_alt_text_modal') &&
missingAltText &&
privacy !== 'direct'
) {
dispatch(
openModal({
modalType: 'CONFIRM_MISSING_ALT_TEXT',
modalProps: {},
}),
);
} else if (quoteToPrivate) {
dispatch(
openModal({
modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
modalProps: {},
}),
);
} else {
dispatch(
submitComposeApi((status: ApiStatusJSON) => {
if (redirectOnSuccess) {
window.location.assign(status.url);
}
}),
);
}
},
);
const urlLikeRegex = /^https?:\/\/[^\s]+\/[^\s]+$/i;
export const processPasteOrDrop = createAppThunk(
(transfer: DataTransfer, { dispatch }) => {
if (transfer.files.length === 1) {
dispatch(uploadCompose(transfer.files));
} else if (transfer.files.length === 0) {
const data = transfer.getData('text/plain');
if (!urlLikeRegex.exec(data)) return;
try {
const url = new URL(data).toString();
void dispatch(pasteLinkCompose({ url }));
} catch {}
}
},
);

View File

@@ -38,7 +38,7 @@ type Language = [string, string, string];
const getFrequentlyUsedLanguages = createSelector(
[
(state: RootState) =>
(state.settings as ImmutableMap<string, unknown>).get(
state.settings.get(
'frequentlyUsedLanguages',
ImmutableMap(),
) as ImmutableMap<string, number>,

View File

@@ -10,9 +10,8 @@ import {
insertEmojiCompose,
uploadCompose,
} from 'mastodon/actions/compose';
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { pasteLinkCompose, PRIVATE_QUOTE_MODAL_ID } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import { me } from 'mastodon/initial_state';
import ComposeForm from '../components/compose_form';

View File

@@ -52,10 +52,7 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const columns = useAppSelector(
(state) =>
(state.settings as ImmutableMap<string, unknown>).get(
'columns',
) as ImmutableList<ColumnMap>,
(state) => state.settings.get('columns') as ImmutableList<ColumnMap>,
);
useEffect(() => {

View File

@@ -1,8 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
-- the settings store is not yet typed */
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';

View File

@@ -174,9 +174,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
const suggestions = useAppSelector((state) => state.suggestions.items);
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const dismissed = useAppSelector(
(state) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]) as boolean,
(state) => !!state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]),
);
useEffect(() => {

View File

@@ -18,9 +18,10 @@ import SettingToggle from './setting_toggle';
const selectNotificationSettings = createAppSelector(
[
(state) =>
(state.settings as Immutable.Map<string, unknown>).get(
'notifications',
) as Immutable.Map<string, Immutable.Map<string, unknown> | boolean>,
state.settings.get('notifications') as Immutable.Map<
string,
Immutable.Map<string, unknown> | boolean
>,
(state) => state.notifications.get('browserPermission') as string,
(state) => state.push_notifications,
],

View File

@@ -93,10 +93,8 @@ export const ColumnsArea = forwardRef<
}
>(({ children, minimalShell, singleColumn }, ref) => {
const renderComposePanel = !useBreakpoint('full');
const columns = useAppSelector((state) =>
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
'columns',
),
const columns = useAppSelector(
(state) => state.settings.get('columns') as List<Record<Column>>,
);
const isModalOpen = useAppSelector(
(state) => !state.modal.get('stack').isEmpty(),

View File

@@ -3,6 +3,7 @@ import { forwardRef, useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { submitCompose } from '@/mastodon/actions/compose';
import { PRIVATE_QUOTE_MODAL_ID } from '@/mastodon/actions/compose_typed';
import { changeSetting } from '@/mastodon/actions/settings';
import { CheckBox } from '@/mastodon/components/check_box';
import { useAppDispatch } from '@/mastodon/store';
@@ -11,8 +12,6 @@ import { ConfirmationModal } from './confirmation_modal';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import classes from './styles.module.css';
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
const messages = defineMessages({
title: {
id: 'confirmations.private_quote_notify.title',

View File

@@ -16,7 +16,7 @@ export const SkipLinks: React.FC<{
}> = ({ multiColumn, onFocusGettingStartedColumn }) => {
const intl = useIntl();
const columnCount = useAppSelector((state) => {
const settings = state.settings as Immutable.Collection<string, unknown>;
const settings = state.settings;
return (settings.get('columns') as Immutable.Map<number, unknown>).size;
});

View File

@@ -165,6 +165,7 @@ const updateFrequentLanguages = (state, language) => state.update('frequentlyUse
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
/** @type {import('@reduxjs/toolkit').Reducer<ImmutableMap<string, unknown>>} */
export default function settings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:

View File

@@ -48,7 +48,12 @@ export const selectSettingsNotificationsShowUnread = (state: RootState) =>
state.settings.getIn(['notifications', 'showUnread']) as boolean;
export const selectNeedsNotificationPermission = (state: RootState) =>
(state.settings.getIn(['notifications', 'alerts']).includes(true) &&
((
state.settings.getIn(['notifications', 'alerts']) as Immutable.Map<
string,
boolean
>
).includes(true) &&
state.notifications.get('browserSupport') &&
state.notifications.get('browserPermission') === 'default' &&
!state.settings.getIn([