Files
ncd-fe/packages/pl-fe/src/actions/statuses.ts
marcin mikołajczak 542ba9ab78 pl-fe: backups, polls reducers
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-11-06 13:06:41 +01:00

411 lines
13 KiB
TypeScript

import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { isLoggedIn } from 'pl-fe/utils/auth';
import { shouldHaveCard } from 'pl-fe/utils/status';
import { getClient } from '../api';
import { setComposeToStatus } from './compose';
import { importEntities } from './importer';
import { deleteFromTimelines } from './timelines';
import type { CreateStatusParams, Status as BaseStatus } from 'pl-api';
import type { Status } from 'pl-fe/normalizers/status';
import type { AppDispatch, RootState } from 'pl-fe/store';
import type { IntlShape } from 'react-intl';
const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST' as const;
const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS' as const;
const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL' as const;
const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST' as const;
const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS' as const;
const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL' as const;
const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST' as const;
const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS' as const;
const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL' as const;
const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST' as const;
const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS' as const;
const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL' as const;
const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST' as const;
const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS' as const;
const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL' as const;
const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST' as const;
const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS' as const;
const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL' as const;
const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST' as const;
const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const;
const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL' as const;
const STATUS_REVEAL_MEDIA = 'STATUS_REVEAL_MEDIA' as const;
const STATUS_HIDE_MEDIA = 'STATUS_HIDE_MEDIA' as const;
const STATUS_EXPAND_SPOILER = 'STATUS_EXPAND_SPOILER' as const;
const STATUS_COLLAPSE_SPOILER = 'STATUS_COLLAPSE_SPOILER' as const;
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST' as const;
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS' as const;
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL' as const;
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO' as const;
const STATUS_UNFILTER = 'STATUS_UNFILTER' as const;
const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE' as const;
const createStatus = (params: CreateStatusParams, idempotencyKey: string, statusId: string | null) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params))
.then((status) => {
// The backend might still be processing the rich media attachment
const expectsCard = status.scheduled_at === null && !status.card && shouldHaveCard(status);
if (status.scheduled_at === null) dispatch(importEntities({ statuses: [{ ...status, expectsCard }] }, { idempotencyKey, withParents: true }));
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId });
// Poll the backend for the updated card
if (expectsCard) {
const delay = 1000;
const poll = (retries = 5) => {
return getClient(getState()).statuses.getStatus(status.id).then(response => {
if (response.card) {
dispatch(importEntities({ statuses: [response] }));
} else if (retries > 0 && response) {
setTimeout(() => poll(retries - 1), delay);
}
}).catch(console.error);
};
setTimeout(() => poll(), delay);
}
return status;
}).catch(error => {
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!statusId });
throw error;
});
};
const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const status = state.statuses[statusId]!;
const poll = status.poll_id ? state.polls[status.poll_id] : undefined;
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
return getClient(state).statuses.getStatusSource(statusId).then(response => {
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false));
useModalsStore.getState().openModal('COMPOSE');
}).catch(error => {
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
});
};
const fetchStatus = (statusId: string, intl?: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_FETCH_REQUEST, statusId });
const params = intl && useSettingsStore.getState().settings.autoTranslate ? {
language: intl.locale,
} : undefined;
return getClient(getState()).statuses.getStatus(statusId, params).then(status => {
dispatch(importEntities({ statuses: [status] }));
dispatch({ type: STATUS_FETCH_SUCCESS, status });
return status;
}).catch(error => {
dispatch({ type: STATUS_FETCH_FAIL, statusId, error, skipAlert: true });
});
};
const deleteStatus = (statusId: string, withRedraft = false) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
const state = getState();
const status = state.statuses[statusId]!;
const poll = status.poll_id ? state.polls[status.poll_id] : undefined;
dispatch({ type: STATUS_DELETE_REQUEST, params: status });
return getClient(state).statuses.deleteStatus(statusId).then(response => {
dispatch({ type: STATUS_DELETE_SUCCESS, statusId });
dispatch(deleteFromTimelines(statusId));
if (withRedraft) {
dispatch(setComposeToStatus(status, poll, response.text || '', response.spoiler_text, response.content_type, withRedraft));
useModalsStore.getState().openModal('COMPOSE');
}
})
.catch(error => {
dispatch({ type: STATUS_DELETE_FAIL, params: status, error });
});
};
const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) =>
dispatch(importEntities({ statuses: [status] }));
const fetchContext = (statusId: string, intl?: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CONTEXT_FETCH_REQUEST, statusId });
const params = intl && useSettingsStore.getState().settings.autoTranslate ? {
language: intl.locale,
} : undefined;
return getClient(getState()).statuses.getContext(statusId, params).then(context => {
const { ancestors, descendants } = context;
const statuses = ancestors.concat(descendants);
dispatch(importEntities({ statuses }));
dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants });
return context;
}).catch(error => {
if (error.response?.status === 404) {
dispatch(deleteFromTimelines(statusId));
}
dispatch({ type: CONTEXT_FETCH_FAIL, statusId, error, skipAlert: true });
});
};
const fetchStatusWithContext = (statusId: string, intl?: IntlShape) =>
async (dispatch: AppDispatch) => Promise.all([
dispatch(fetchContext(statusId, intl)),
dispatch(fetchStatus(statusId, intl)),
]);
const muteStatus = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch({ type: STATUS_MUTE_REQUEST, statusId });
return getClient(getState()).statuses.muteStatus(statusId).then((status) => {
dispatch({ type: STATUS_MUTE_SUCCESS, statusId });
}).catch(error => {
dispatch({ type: STATUS_MUTE_FAIL, statusId, error });
});
};
const unmuteStatus = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch({ type: STATUS_UNMUTE_REQUEST, statusId });
return getClient(getState()).statuses.unmuteStatus(statusId).then(() => {
dispatch({ type: STATUS_UNMUTE_SUCCESS, statusId });
}).catch(error => {
dispatch({ type: STATUS_UNMUTE_FAIL, statusId, error });
});
};
const toggleMuteStatus = (status: Pick<Status, 'id' | 'muted'>) =>
(dispatch: AppDispatch) => {
if (status.muted) {
dispatch(unmuteStatus(status.id));
} else {
dispatch(muteStatus(status.id));
}
};
const hideStatusMedia = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_HIDE_MEDIA,
statusIds,
};
};
const revealStatusMedia = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_REVEAL_MEDIA,
statusIds,
};
};
const toggleStatusMediaHidden = (status: Pick<Status, 'id' | 'hidden'>) => {
if (status.hidden) {
return revealStatusMedia(status.id);
} else {
return hideStatusMedia(status.id);
}
};
const collapseStatusSpoiler = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_COLLAPSE_SPOILER,
statusIds,
};
};
const expandStatusSpoiler = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_EXPAND_SPOILER,
statusIds,
};
};
let TRANSLATIONS_QUEUE: Set<string> = new Set();
let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null;
const translateStatus = (statusId: string, targetLanguage: string, lazy?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const client = getClient(getState);
const features = client.features;
dispatch({ type: STATUS_TRANSLATE_REQUEST, statusId });
const handleTranslateMany = () => {
const copy = [...TRANSLATIONS_QUEUE];
TRANSLATIONS_QUEUE = new Set();
if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT);
return client.statuses.translateStatuses(copy, targetLanguage).then((response) => {
response.forEach((translation) => {
dispatch({
type: STATUS_TRANSLATE_SUCCESS,
statusId: translation.id,
translation: translation,
});
copy
.filter((statusId) => !response.some(({ id }) => id === statusId))
.forEach((statusId) => dispatch({
type: STATUS_TRANSLATE_FAIL,
statusId,
}));
});
}).catch(error => {
dispatch({
type: STATUS_TRANSLATE_FAIL,
statusId,
error,
});
});
};
if (features.lazyTranslations && lazy) {
TRANSLATIONS_QUEUE.add(statusId);
if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT);
TRANSLATIONS_TIMEOUT = setTimeout(() => handleTranslateMany(), 3000);
} else if (features.lazyTranslations && TRANSLATIONS_QUEUE.size) {
TRANSLATIONS_QUEUE.add(statusId);
handleTranslateMany();
} else {
return client.statuses.translateStatus(statusId, targetLanguage).then(response => {
dispatch({
type: STATUS_TRANSLATE_SUCCESS,
statusId,
translation: response,
});
}).catch(error => {
dispatch({
type: STATUS_TRANSLATE_FAIL,
statusId,
error,
});
});
}
};
const undoStatusTranslation = (statusId: string) => ({
type: STATUS_TRANSLATE_UNDO,
statusId,
});
const unfilterStatus = (statusId: string) => ({
type: STATUS_UNFILTER,
statusId,
});
const changeStatusLanguage = (statusId: string, language: string) => ({
type: STATUS_LANGUAGE_CHANGE,
statusId,
language,
});
type StatusesAction =
| ReturnType<typeof undoStatusTranslation>
| ReturnType<typeof unfilterStatus>
| ReturnType<typeof changeStatusLanguage>;
export {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
STATUS_CREATE_FAIL,
STATUS_FETCH_SOURCE_REQUEST,
STATUS_FETCH_SOURCE_SUCCESS,
STATUS_FETCH_SOURCE_FAIL,
STATUS_FETCH_REQUEST,
STATUS_FETCH_SUCCESS,
STATUS_FETCH_FAIL,
STATUS_DELETE_REQUEST,
STATUS_DELETE_SUCCESS,
STATUS_DELETE_FAIL,
CONTEXT_FETCH_REQUEST,
CONTEXT_FETCH_SUCCESS,
CONTEXT_FETCH_FAIL,
STATUS_MUTE_REQUEST,
STATUS_MUTE_SUCCESS,
STATUS_MUTE_FAIL,
STATUS_UNMUTE_REQUEST,
STATUS_UNMUTE_SUCCESS,
STATUS_UNMUTE_FAIL,
STATUS_REVEAL_MEDIA,
STATUS_HIDE_MEDIA,
STATUS_EXPAND_SPOILER,
STATUS_COLLAPSE_SPOILER,
STATUS_TRANSLATE_REQUEST,
STATUS_TRANSLATE_SUCCESS,
STATUS_TRANSLATE_FAIL,
STATUS_TRANSLATE_UNDO,
STATUS_UNFILTER,
STATUS_LANGUAGE_CHANGE,
createStatus,
editStatus,
fetchStatus,
deleteStatus,
updateStatus,
fetchContext,
fetchStatusWithContext,
muteStatus,
unmuteStatus,
toggleMuteStatus,
hideStatusMedia,
revealStatusMedia,
toggleStatusMediaHidden,
expandStatusSpoiler,
collapseStatusSpoiler,
translateStatus,
undoStatusTranslation,
unfilterStatus,
changeStatusLanguage,
type StatusesAction,
};