signal-desktop/ts/state/ducks/mediaGallery.ts
2022-12-20 09:50:23 -08:00

225 lines
6.3 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,
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_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';
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;
};
};
export type MediaGalleryStateType = {
documents: Array<MediaItemType>;
media: Array<MediaType>;
};
const LOAD_MEDIA_ITEMS = 'mediaGallery/LOAD_MEDIA_ITEMS';
type LoadMediaItemslActionType = {
type: typeof LOAD_MEDIA_ITEMS;
payload: {
documents: Array<MediaItemType>;
media: Array<MediaType>;
};
};
type MediaGalleryActionType =
| ConversationUnloadedActionType
| LoadMediaItemslActionType
| 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 ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
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.MessageController.register(message.id, message);
if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) {
const upgradedMsgAttributes = await upgradeMessageSchema(message);
model.set(upgradedMsgAttributes);
await dataInterface.saveMessage(upgradedMsgAttributes, { ourUuid });
}
})
);
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({
uuid: message.sourceUuid,
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_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;
}