Signal-Desktop/ts/state/ducks/mediaGallery.ts
2023-10-03 17:12:57 -07:00

251 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
import type { AttachmentType } from '../../types/Attachment';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type {
ConversationUnloadedActionType,
MessageChangedActionType,
MessageDeletedActionType,
MessageExpiredActionType,
} from './conversations';
import type { MIMEType } from '../../types/MIME';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType as RootStateType } from '../reducer';
import dataInterface from '../../sql/Client';
import {
CONVERSATION_UNLOADED,
MESSAGE_CHANGED,
MESSAGE_DELETED,
MESSAGE_EXPIRED,
} from './conversations';
import { VERSION_NEEDED_FOR_DISPLAY } from '../../types/Message2';
import { isDownloading, hasFailed } from '../../types/Attachment';
import { isNotNil } from '../../util/isNotNil';
import { useBoundActions } from '../../hooks/useBoundActions';
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type MediaType = {
path: string;
objectURL: string;
thumbnailObjectUrl?: string;
contentType: MIMEType;
index: number;
attachment: AttachmentType;
message: {
attachments: Array<AttachmentType>;
conversationId: string;
id: string;
received_at: number;
received_at_ms: number;
sent_at: number;
};
};
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type MediaGalleryStateType = {
documents: Array<MediaItemType>;
media: Array<MediaType>;
};
const LOAD_MEDIA_ITEMS = 'mediaGallery/LOAD_MEDIA_ITEMS';
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type LoadMediaItemslActionType = {
type: typeof LOAD_MEDIA_ITEMS;
payload: {
documents: Array<MediaItemType>;
media: Array<MediaType>;
};
};
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type MediaGalleryActionType =
| ConversationUnloadedActionType
| LoadMediaItemslActionType
| MessageChangedActionType
| MessageDeletedActionType
| MessageExpiredActionType;
function loadMediaItems(
conversationId: string
): ThunkAction<void, RootStateType, unknown, LoadMediaItemslActionType> {
return async dispatch => {
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
window.Signal.Migrations;
// We fetch more documents than media as they dont require to be loaded
// into memory right away. Revisit this once we have infinite scrolling:
const DEFAULT_MEDIA_FETCH_COUNT = 50;
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
const ourAci = window.textsecure.storage.user.getCheckedAci();
const rawMedia = await dataInterface.getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: DEFAULT_MEDIA_FETCH_COUNT,
}
);
const rawDocuments = await dataInterface.getMessagesWithFileAttachments(
conversationId,
{
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
}
);
// First we upgrade these messages to ensure that they have thumbnails
await Promise.all(
rawMedia.map(async message => {
const { schemaVersion } = message;
const model = window.MessageCache.__DEPRECATED$register(
message.id,
message,
'loadMediaItems'
);
if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) {
const upgradedMsgAttributes = await upgradeMessageSchema(message);
model.set(upgradedMsgAttributes);
await dataInterface.saveMessage(upgradedMsgAttributes, { ourAci });
}
})
);
const media: Array<MediaType> = rawMedia
.flatMap(message => {
return (message.attachments || []).map(
(
attachment: AttachmentType,
index: number
): MediaType | undefined => {
if (
!attachment.path ||
!attachment.thumbnail ||
isDownloading(attachment) ||
hasFailed(attachment)
) {
return;
}
const { thumbnail } = attachment;
return {
path: attachment.path,
objectURL: getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail?.path
? getAbsoluteAttachmentPath(thumbnail.path)
: undefined,
contentType: attachment.contentType,
index,
attachment,
message: {
attachments: message.attachments || [],
conversationId:
window.ConversationController.lookupOrCreate({
serviceId: message.sourceServiceId,
e164: message.source,
reason: 'conversation_view.showAllMedia',
})?.id || message.conversationId,
id: message.id,
received_at: message.received_at,
received_at_ms: Number(message.received_at_ms),
sent_at: message.sent_at,
},
};
}
);
})
.filter(isNotNil);
// Unlike visual media, only one non-image attachment is supported
const documents: Array<MediaItemType> = rawDocuments
.map(message => {
const attachments = message.attachments || [];
const attachment = attachments[0];
if (!attachment) {
return;
}
return {
contentType: attachment.contentType,
index: 0,
attachment,
message: {
...message,
attachments: [attachment],
},
};
})
.filter(isNotNil);
dispatch({
type: LOAD_MEDIA_ITEMS,
payload: {
documents,
media,
},
});
};
}
export const actions = {
loadMediaItems,
};
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function getEmptyState(): MediaGalleryStateType {
return {
documents: [],
media: [],
};
}
export function reducer(
state: Readonly<MediaGalleryStateType> = getEmptyState(),
action: Readonly<MediaGalleryActionType>
): MediaGalleryStateType {
if (action.type === LOAD_MEDIA_ITEMS) {
return {
...state,
...action.payload,
};
}
if (action.type === MESSAGE_CHANGED) {
if (!action.payload.data.deletedForEveryone) {
return state;
}
return {
...state,
media: state.media.filter(item => item.message.id !== action.payload.id),
documents: state.documents.filter(
item => item.message.id !== action.payload.id
),
};
}
if (action.type === MESSAGE_DELETED || action.type === MESSAGE_EXPIRED) {
return {
...state,
media: state.media.filter(item => item.message.id !== action.payload.id),
documents: state.documents.filter(
item => item.message.id !== action.payload.id
),
};
}
if (action.type === CONVERSATION_UNLOADED) {
return getEmptyState();
}
return state;
}