mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-28 10:46:36 -05:00
Redesign composer thunks (#39945)
This commit is contained in:
@@ -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 {}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user