From 537897dedb916ca192880eeb66839a3f66729cfe Mon Sep 17 00:00:00 2001 From: William Grant Date: Wed, 18 Oct 2023 17:13:45 +1100 Subject: [PATCH] feat: added duration to media attachments added showLightboxFromAttachmentProps for future use --- .../conversation/SessionConversation.tsx | 40 +++++++---- .../message-content/MessageAttachment.tsx | 41 +++++++++++ ts/types/Attachment.ts | 7 +- ts/types/attachments/VisualAttachment.ts | 68 +++++++++++++++++++ 4 files changed, 140 insertions(+), 16 deletions(-) diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index e23131518..1626979e4 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -38,6 +38,8 @@ import { MIME } from '../../types'; import { AttachmentTypeWithPath } from '../../types/Attachment'; import { THUMBNAIL_CONTENT_TYPE, + getAudioDuration, + getVideoDuration, makeImageThumbnailBuffer, makeVideoScreenshot, } from '../../types/attachments/VisualAttachment'; @@ -52,6 +54,7 @@ import { NoMessageInConversation } from './SubtleNotification'; import { ConversationHeaderWithDetails } from './header/ConversationHeader'; import { MessageDetail } from './message/message-item/MessageDetail'; +import { isAudio } from '../../types/MIME'; import { HTMLDirection } from '../../util/i18n'; import { NoticeBanner } from '../NoticeBanner'; import { SessionSpinner } from '../basic/SessionSpinner'; @@ -452,19 +455,25 @@ export class SessionConversation extends React.Component { const attachmentWithVideoPreview = await renderVideoPreview(contentType, file, fileName); this.addAttachments([attachmentWithVideoPreview]); } else { - this.addAttachments([ - { - file, - size: file.size, - contentType, - fileName, - url: '', - isVoiceMessage: false, - fileSize: null, - screenshot: null, - thumbnail: null, - }, - ]); + const attachment: StagedAttachmentType = { + file, + size: file.size, + contentType, + fileName, + url: '', + isVoiceMessage: false, + fileSize: null, + screenshot: null, + thumbnail: null, + }; + + if (isAudio(contentType)) { + const objectUrl = URL.createObjectURL(file); + const duration = await getAudioDuration({ objectUrl, contentType }); + attachment.duration = duration; + } + + this.addAttachments([attachment]); } } catch (e) { window?.log?.error( @@ -567,6 +576,10 @@ const renderVideoPreview = async (contentType: string, file: File, fileName: str objectUrl, contentType: type, }); + const duration = await getVideoDuration({ + objectUrl, + contentType: type, + }); const data = await blobToArrayBuffer(thumbnail); const url = arrayBufferToObjectURL({ data, @@ -577,6 +590,7 @@ const renderVideoPreview = async (contentType: string, file: File, fileName: str size: file.size, fileName, contentType, + duration, videoUrl: objectUrl, url, isVoiceMessage: false, diff --git a/ts/components/conversation/message/message-content/MessageAttachment.tsx b/ts/components/conversation/message/message-content/MessageAttachment.tsx index 0b5440fca..4605b6b41 100644 --- a/ts/components/conversation/message/message-content/MessageAttachment.tsx +++ b/ts/components/conversation/message/message-content/MessageAttachment.tsx @@ -218,6 +218,47 @@ function attachmentIsAttachmentTypeWithPath(attac: any): attac is AttachmentType return attac.path !== undefined; } +export async function showLightboxFromAttachmentProps( + messageId: string, + selected: AttachmentTypeWithPath | AttachmentType | PropsForAttachment +) { + const found = await Data.getMessageById(messageId); + if (!found) { + window.log.warn(`showLightboxFromAttachmentProps Message not found ${messageId}}`); + return; + } + + const msgAttachments = found.getPropsForMessage().attachments; + + let index = -1; + + const media = (msgAttachments || []).map(attachmentForMedia => { + index++; + const messageTimestamp = + found.get('timestamp') || found.get('serverTimestamp') || found.get('received_at'); + + return { + index: clone(index), + objectURL: attachmentForMedia.url || undefined, + contentType: attachmentForMedia.contentType, + attachment: attachmentForMedia, + messageSender: found.getSource(), + messageTimestamp, + messageId, + }; + }); + + if (attachmentIsAttachmentTypeWithPath(selected)) { + const lightBoxOptions: LightBoxOptions = { + media: media as any, + attachment: selected, + }; + window.inboxStore?.dispatch(showLightBox(lightBoxOptions)); + } else { + window.log.warn('Attachment is not of the right type'); + } +} + const onClickAttachment = async (onClickProps: { attachment: AttachmentTypeWithPath | AttachmentType; messageId: string; diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 5c2559d9a..df85021aa 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -1,11 +1,11 @@ -import moment from 'moment'; import { isUndefined, padStart } from 'lodash'; +import moment from 'moment'; -import * as MIME from './MIME'; -import { saveURLAsFile } from '../util/saveURLAsFile'; import { SignalService } from '../protobuf'; import { isImageTypeSupported, isVideoTypeSupported } from '../util/GoogleChrome'; import { ATTACHMENT_DEFAULT_MAX_SIDE } from '../util/attachmentsUtil'; +import { saveURLAsFile } from '../util/saveURLAsFile'; +import * as MIME from './MIME'; import { THUMBNAIL_SIDE } from './attachments/VisualAttachment'; const MAX_WIDTH = THUMBNAIL_SIDE; @@ -29,6 +29,7 @@ export interface AttachmentType { pending?: boolean; width?: number; height?: number; + duration?: string; screenshot: { height: number; width: number; diff --git a/ts/types/attachments/VisualAttachment.ts b/ts/types/attachments/VisualAttachment.ts index 3d24d52c4..49d2be223 100644 --- a/ts/types/attachments/VisualAttachment.ts +++ b/ts/types/attachments/VisualAttachment.ts @@ -2,6 +2,7 @@ /* global document, URL, Blob */ import { blobToArrayBuffer, dataURLToBlob } from 'blob-util'; +import moment from 'moment'; import { toLogFormat } from './Errors'; import { @@ -11,6 +12,7 @@ import { import { ToastUtils } from '../../session/utils'; import { GoogleChrome } from '../../util'; import { autoScaleForAvatar, autoScaleForThumbnail } from '../../util/attachmentsUtil'; +import { isAudio } from '../MIME'; export const THUMBNAIL_SIDE = 200; export const THUMBNAIL_CONTENT_TYPE = 'image/png'; @@ -106,6 +108,72 @@ export const makeVideoScreenshot = async ({ }); }); +// TODO need to confirm this works +export async function getVideoDuration({ + objectUrl, + contentType, +}: { + objectUrl: string; + contentType: string; +}): Promise { + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + + video.addEventListener('loadedmetadata', () => { + const duration = moment.duration(video.duration, 'seconds'); + const durationString = moment.utc(duration.asMilliseconds()).format('m:ss'); + resolve(durationString); + }); + + video.addEventListener('error', error => { + reject(error); + }); + + void getDecryptedMediaUrl(objectUrl, contentType, false) + .then(decryptedUrl => { + video.src = decryptedUrl; + }) + .catch(err => { + reject(err); + }); + }); +} + +// TODO need to confirm this works +export async function getAudioDuration({ + objectUrl, + contentType, +}: { + objectUrl: string; + contentType: string; +}): Promise { + if (!isAudio(contentType)) { + throw new Error('getAudioDuration can only be called with audio content type'); + } + + return new Promise((resolve, reject) => { + const audio = document.createElement('audio'); + + audio.addEventListener('loadedmetadata', () => { + const duration = moment.duration(audio.duration, 'seconds'); + const durationString = moment.utc(duration.asMilliseconds()).format('m:ss'); + resolve(durationString); + }); + + audio.addEventListener('error', error => { + reject(error); + }); + + void getDecryptedMediaUrl(objectUrl, contentType, false) + .then(decryptedUrl => { + audio.src = decryptedUrl; + }) + .catch(err => { + reject(err); + }); + }); +} + export const makeObjectUrl = (data: ArrayBufferLike, contentType: string) => { const blob = new Blob([data], { type: contentType,