signal-desktop/ts/state/ducks/callHistory.ts

321 lines
8.7 KiB
TypeScript
Raw Normal View History

2023-08-09 00:53:06 +00:00
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
2024-08-22 18:12:25 +00:00
import { debounce, omit } from 'lodash';
2023-08-09 00:53:06 +00:00
import type { StateType as RootStateType } from '../reducer';
import {
clearCallHistoryDataAndSync,
markAllCallHistoryReadAndSync,
} from '../../util/callDisposition';
2023-08-09 00:53:06 +00:00
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { ToastActionType } from './toast';
import { showToast } from './toast';
2024-07-22 18:16:33 +00:00
import { DataReader, DataWriter } from '../../sql/Client';
2023-08-09 00:53:06 +00:00
import { ToastType } from '../../types/Toast';
2024-10-09 16:35:24 +00:00
import {
ClearCallHistoryResult,
type CallHistoryDetails,
} from '../../types/CallDisposition';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import { drop } from '../../util/drop';
2024-06-26 00:58:38 +00:00
import {
getCallHistoryLatestCall,
getCallHistorySelector,
} from '../selectors/callHistory';
import {
getCallsHistoryForRedux,
getCallsHistoryUnreadCountForRedux,
loadCallHistory,
} from '../../services/callHistoryLoader';
import { makeLookup } from '../../util/makeLookup';
2024-10-09 16:35:24 +00:00
import { missingCaseError } from '../../util/missingCaseError';
import { getIntl } from '../selectors/user';
import { ButtonVariant } from '../../components/Button';
import type { ShowErrorModalActionType } from './globalModals';
import { SHOW_ERROR_MODAL } from './globalModals';
2023-08-09 00:53:06 +00:00
export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed.
edition: number;
unreadCount: number;
2023-08-09 00:53:06 +00:00
callHistoryByCallId: Record<string, CallHistoryDetails>;
}>;
const CALL_HISTORY_ADD = 'callHistory/ADD';
const CALL_HISTORY_REMOVE = 'callHistory/REMOVE';
const CALL_HISTORY_RESET = 'callHistory/RESET';
const CALL_HISTORY_RELOAD = 'callHistory/RELOAD';
const CALL_HISTORY_UPDATE_UNREAD = 'callHistory/UPDATE_UNREAD';
2023-08-09 00:53:06 +00:00
export type CallHistoryAdd = ReadonlyDeep<{
type: typeof CALL_HISTORY_ADD;
2023-08-09 00:53:06 +00:00
payload: CallHistoryDetails;
}>;
export type CallHistoryRemove = ReadonlyDeep<{
type: typeof CALL_HISTORY_REMOVE;
payload: CallHistoryDetails['callId'];
}>;
export type CallHistoryReset = ReadonlyDeep<{
type: typeof CALL_HISTORY_RESET;
2023-08-09 00:53:06 +00:00
}>;
export type CallHistoryReload = ReadonlyDeep<{
type: typeof CALL_HISTORY_RELOAD;
payload: {
callsHistory: ReadonlyArray<CallHistoryDetails>;
callsHistoryUnreadCount: number;
};
}>;
export type CallHistoryUpdateUnread = ReadonlyDeep<{
type: typeof CALL_HISTORY_UPDATE_UNREAD;
payload: number;
}>;
2023-08-09 00:53:06 +00:00
export type CallHistoryAction = ReadonlyDeep<
| CallHistoryAdd
| CallHistoryRemove
| CallHistoryReset
| CallHistoryReload
| CallHistoryUpdateUnread
2023-08-09 00:53:06 +00:00
>;
export function getEmptyState(): CallHistoryState {
return {
edition: 0,
unreadCount: 0,
2023-08-09 00:53:06 +00:00
callHistoryByCallId: {},
};
}
const updateCallHistoryUnreadCountDebounced = debounce(
async (
dispatch: ThunkDispatch<RootStateType, unknown, CallHistoryUpdateUnread>
) => {
try {
2024-07-22 18:16:33 +00:00
const unreadCount = await DataReader.getCallHistoryUnreadCount();
dispatch({ type: CALL_HISTORY_UPDATE_UNREAD, payload: unreadCount });
} catch (error) {
log.error(
'Error updating call history unread count',
Errors.toLogFormat(error)
);
}
},
2024-08-22 18:12:25 +00:00
300
);
function updateCallHistoryUnreadCount(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryUpdateUnread
> {
return async dispatch => {
await updateCallHistoryUnreadCountDebounced(dispatch);
};
}
function markCallHistoryRead(
conversationId: string,
callId: string
): ThunkAction<void, RootStateType, unknown, CallHistoryUpdateUnread> {
return async dispatch => {
try {
2024-07-22 18:16:33 +00:00
await DataWriter.markCallHistoryRead(callId);
drop(window.ConversationController.get(conversationId)?.updateUnread());
} catch (error) {
log.error(
'markCallHistoryRead: Error marking call history read',
Errors.toLogFormat(error)
);
} finally {
dispatch(updateCallHistoryUnreadCount());
}
};
}
2024-06-26 00:58:38 +00:00
export function markCallHistoryReadInConversation(
callId: string
): ThunkAction<void, RootStateType, unknown, CallHistoryUpdateUnread> {
return async (dispatch, getState) => {
const callHistorySelector = getCallHistorySelector(getState());
const callHistory = callHistorySelector(callId);
if (callHistory == null) {
return;
}
try {
await markAllCallHistoryReadAndSync(callHistory, true);
} finally {
dispatch(updateCallHistoryUnreadCount());
}
};
}
function markCallsTabViewed(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryUpdateUnread
> {
2024-06-26 00:58:38 +00:00
return async (dispatch, getState) => {
const latestCall = getCallHistoryLatestCall(getState());
if (latestCall != null) {
await markAllCallHistoryReadAndSync(latestCall, false);
dispatch(updateCallHistoryUnreadCount());
}
};
}
2024-06-10 15:23:43 +00:00
export function addCallHistory(
callHistory: CallHistoryDetails
): CallHistoryAdd {
2023-08-09 00:53:06 +00:00
return {
type: CALL_HISTORY_ADD,
2023-08-09 00:53:06 +00:00
payload: callHistory,
};
}
function removeCallHistory(
callId: CallHistoryDetails['callId']
): CallHistoryRemove {
return {
type: CALL_HISTORY_REMOVE,
payload: callId,
};
}
function resetCallHistory(): CallHistoryReset {
return { type: CALL_HISTORY_RESET };
}
2023-08-09 00:53:06 +00:00
function clearAllCallHistory(): ThunkAction<
void,
RootStateType,
unknown,
2024-10-09 16:35:24 +00:00
CallHistoryReset | ToastActionType | ShowErrorModalActionType
2023-08-09 00:53:06 +00:00
> {
2024-06-26 00:58:38 +00:00
return async (dispatch, getState) => {
try {
2024-06-26 00:58:38 +00:00
const latestCall = getCallHistoryLatestCall(getState());
2024-10-09 16:35:24 +00:00
if (latestCall == null) {
return;
}
const result = await clearCallHistoryDataAndSync(latestCall);
if (result === ClearCallHistoryResult.Success) {
2024-06-26 00:58:38 +00:00
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
2024-10-09 16:35:24 +00:00
} else if (result === ClearCallHistoryResult.Error) {
const i18n = getIntl(getState());
dispatch({
type: SHOW_ERROR_MODAL,
payload: {
title: null,
description: i18n('icu:CallsTab__ClearCallHistoryError'),
buttonVariant: ButtonVariant.Primary,
},
});
} else if (result === ClearCallHistoryResult.ErrorDeletingCallLinks) {
const i18n = getIntl(getState());
dispatch({
type: SHOW_ERROR_MODAL,
payload: {
title: null,
description: i18n(
'icu:CallsTab__ClearCallHistoryError--call-links'
),
buttonVariant: ButtonVariant.Primary,
},
});
} else {
throw missingCaseError(result);
2024-06-26 00:58:38 +00:00
}
} catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error));
} finally {
// Just force a reload, even if the clear failed.
dispatch(reloadCallHistory());
}
};
}
export function reloadCallHistory(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryReload
> {
return async dispatch => {
try {
await loadCallHistory();
const callsHistory = getCallsHistoryForRedux();
const callsHistoryUnreadCount = getCallsHistoryUnreadCountForRedux();
dispatch({
type: CALL_HISTORY_RELOAD,
payload: { callsHistory, callsHistoryUnreadCount },
});
} catch (error) {
log.error('Error reloading call history', Errors.toLogFormat(error));
}
2023-08-09 00:53:06 +00:00
};
}
export const actions = {
addCallHistory,
removeCallHistory,
resetCallHistory,
reloadCallHistory,
2023-08-09 00:53:06 +00:00
clearAllCallHistory,
updateCallHistoryUnreadCount,
markCallHistoryRead,
markCallsTabViewed,
2023-08-09 00:53:06 +00:00
};
export const useCallHistoryActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function reducer(
state: CallHistoryState = getEmptyState(),
action: CallHistoryAction
): CallHistoryState {
switch (action.type) {
case CALL_HISTORY_RESET:
2023-08-09 00:53:06 +00:00
return { ...state, edition: state.edition + 1, callHistoryByCallId: {} };
case CALL_HISTORY_ADD:
2023-08-09 00:53:06 +00:00
return {
...state,
edition: state.edition + 1,
2023-08-09 00:53:06 +00:00
callHistoryByCallId: {
...state.callHistoryByCallId,
[action.payload.callId]: action.payload,
},
};
case CALL_HISTORY_REMOVE:
return {
...state,
edition: state.edition + 1,
callHistoryByCallId: omit(state.callHistoryByCallId, action.payload),
};
case CALL_HISTORY_UPDATE_UNREAD:
return {
...state,
unreadCount: action.payload,
};
case CALL_HISTORY_RELOAD:
return {
edition: state.edition + 1,
unreadCount: action.payload.callsHistoryUnreadCount,
callHistoryByCallId: makeLookup(action.payload.callsHistory, 'callId'),
};
2023-08-09 00:53:06 +00:00
default:
return state;
}
}