mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
362 lines
9 KiB
TypeScript
362 lines
9 KiB
TypeScript
import moment from 'moment';
|
|
import { isUndefined, padStart } from 'lodash';
|
|
|
|
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 { THUMBNAIL_SIDE } from './attachments/VisualAttachment';
|
|
|
|
const MAX_WIDTH = THUMBNAIL_SIDE;
|
|
const MAX_HEIGHT = THUMBNAIL_SIDE;
|
|
const MIN_WIDTH = THUMBNAIL_SIDE;
|
|
const MIN_HEIGHT = THUMBNAIL_SIDE;
|
|
|
|
// Used for display
|
|
|
|
export interface AttachmentType {
|
|
caption?: string;
|
|
contentType: MIME.MIMEType;
|
|
fileName: string;
|
|
/** Not included in protobuf, needs to be pulled from flags */
|
|
isVoiceMessage?: boolean;
|
|
/** For messages not already on disk, this will be a data url */
|
|
url: string;
|
|
videoUrl?: string;
|
|
size?: number;
|
|
fileSize: string | null;
|
|
pending?: boolean;
|
|
width?: number;
|
|
height?: number;
|
|
screenshot: {
|
|
height: number;
|
|
width: number;
|
|
url?: string;
|
|
contentType: MIME.MIMEType;
|
|
} | null;
|
|
thumbnail: {
|
|
height: number;
|
|
width: number;
|
|
url?: string;
|
|
contentType: MIME.MIMEType;
|
|
} | null;
|
|
}
|
|
|
|
export interface AttachmentTypeWithPath extends AttachmentType {
|
|
path: string;
|
|
id: number;
|
|
flags?: number;
|
|
error?: any;
|
|
|
|
screenshot: {
|
|
height: number;
|
|
width: number;
|
|
url?: string;
|
|
contentType: MIME.MIMEType;
|
|
path?: string;
|
|
} | null;
|
|
thumbnail: {
|
|
height: number;
|
|
width: number;
|
|
url?: string;
|
|
contentType: MIME.MIMEType;
|
|
path?: string;
|
|
} | null;
|
|
}
|
|
|
|
// UI-focused functions
|
|
|
|
export function getExtensionForDisplay({
|
|
fileName,
|
|
contentType,
|
|
}: {
|
|
fileName: string;
|
|
contentType: MIME.MIMEType;
|
|
}): string | undefined {
|
|
if (fileName && fileName.indexOf('.') >= 0) {
|
|
const lastPeriod = fileName.lastIndexOf('.');
|
|
const extension = fileName.slice(lastPeriod + 1);
|
|
if (extension.length) {
|
|
return extension;
|
|
}
|
|
}
|
|
|
|
if (!contentType) {
|
|
return;
|
|
}
|
|
|
|
const slash = contentType.indexOf('/');
|
|
if (slash >= 0) {
|
|
return contentType.slice(slash + 1);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
export function isAudio(attachments?: Array<AttachmentType>) {
|
|
return (
|
|
attachments &&
|
|
attachments[0] &&
|
|
attachments[0].contentType &&
|
|
MIME.isAudio(attachments[0].contentType)
|
|
);
|
|
}
|
|
|
|
export function canDisplayImage(attachments?: Array<AttachmentType>) {
|
|
const { height, width } =
|
|
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
|
|
|
return Boolean(
|
|
height &&
|
|
height > 0 &&
|
|
height <= ATTACHMENT_DEFAULT_MAX_SIDE &&
|
|
width &&
|
|
width > 0 &&
|
|
width <= ATTACHMENT_DEFAULT_MAX_SIDE
|
|
);
|
|
}
|
|
|
|
export function getThumbnailUrl(attachment: AttachmentType): string {
|
|
if (attachment.thumbnail && attachment.thumbnail.url) {
|
|
return attachment.thumbnail.url;
|
|
}
|
|
|
|
return getUrl(attachment);
|
|
}
|
|
|
|
export function getUrl(attachment: AttachmentType): string {
|
|
if (attachment.screenshot && attachment.screenshot.url) {
|
|
return attachment.screenshot.url as string;
|
|
}
|
|
|
|
return attachment.url;
|
|
}
|
|
|
|
export function isImage(attachments?: Array<AttachmentType>) {
|
|
return (
|
|
attachments &&
|
|
attachments[0] &&
|
|
attachments[0].contentType &&
|
|
isImageTypeSupported(attachments[0].contentType)
|
|
);
|
|
}
|
|
|
|
export function isImageAttachment(attachment: AttachmentType): boolean {
|
|
return Boolean(
|
|
attachment && attachment.contentType && isImageTypeSupported(attachment.contentType)
|
|
);
|
|
}
|
|
export function hasImage(attachments?: Array<AttachmentType>): boolean {
|
|
return Boolean(attachments && attachments[0] && (attachments[0].url || attachments[0].pending));
|
|
}
|
|
|
|
export function isVideo(attachments?: Array<AttachmentType>): boolean {
|
|
return Boolean(attachments && isVideoAttachment(attachments[0]));
|
|
}
|
|
|
|
export function isVideoAttachment(attachment?: AttachmentType): boolean {
|
|
return Boolean(
|
|
!!attachment && !!attachment.contentType && isVideoTypeSupported(attachment.contentType)
|
|
);
|
|
}
|
|
|
|
export function hasVideoScreenshot(attachments?: Array<AttachmentType>): boolean {
|
|
const firstAttachment = attachments ? attachments[0] : null;
|
|
return Boolean(firstAttachment?.screenshot?.url);
|
|
}
|
|
|
|
type DimensionsType = {
|
|
height: number;
|
|
width: number;
|
|
};
|
|
|
|
export async function arrayBufferFromFile(file: any): Promise<ArrayBuffer> {
|
|
return new Promise((resolve, reject) => {
|
|
const FR = new FileReader();
|
|
FR.onload = (e: any) => {
|
|
resolve(e.target.result);
|
|
};
|
|
FR.onerror = reject;
|
|
FR.onabort = reject;
|
|
FR.readAsArrayBuffer(file);
|
|
});
|
|
}
|
|
|
|
export function getImageDimensionsInAttachment(attachment: AttachmentType): DimensionsType {
|
|
const { height, width } = attachment;
|
|
if (!height || !width) {
|
|
return {
|
|
height: MIN_HEIGHT,
|
|
width: MIN_WIDTH,
|
|
};
|
|
}
|
|
|
|
const aspectRatio = height / width;
|
|
const targetWidth = Math.max(Math.min(MAX_WIDTH, width), MIN_WIDTH);
|
|
const candidateHeight = Math.round(targetWidth * aspectRatio);
|
|
|
|
return {
|
|
width: targetWidth,
|
|
height: Math.max(Math.min(MAX_HEIGHT, candidateHeight), MIN_HEIGHT),
|
|
};
|
|
}
|
|
|
|
export function areAllAttachmentsVisual(attachments?: Array<AttachmentType>): boolean {
|
|
if (!attachments) {
|
|
return false;
|
|
}
|
|
|
|
const max = attachments.length;
|
|
for (let i = 0; i < max; i += 1) {
|
|
const attachment = attachments[i];
|
|
if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function getAlt(attachment: AttachmentType): string {
|
|
return isVideoAttachment(attachment)
|
|
? window.i18n('videoAttachmentAlt')
|
|
: window.i18n('imageAttachmentAlt');
|
|
}
|
|
|
|
// Migration-related attachment stuff
|
|
|
|
export type Attachment = {
|
|
fileName?: string;
|
|
caption?: string;
|
|
flags?: SignalService.AttachmentPointer.Flags;
|
|
contentType?: MIME.MIMEType;
|
|
size?: number;
|
|
width?: number;
|
|
height?: number;
|
|
data: ArrayBuffer;
|
|
} & Partial<AttachmentSchemaVersion3>;
|
|
|
|
interface AttachmentSchemaVersion3 {
|
|
path: string;
|
|
}
|
|
|
|
export const isVisualMedia = (attachment: Attachment): boolean => {
|
|
const { contentType } = attachment;
|
|
|
|
if (isUndefined(contentType)) {
|
|
return false;
|
|
}
|
|
|
|
if (isVoiceMessage(attachment)) {
|
|
return false;
|
|
}
|
|
|
|
return MIME.isImage(contentType) || MIME.isVideo(contentType);
|
|
};
|
|
|
|
export const isFile = (attachment: Attachment): boolean => {
|
|
const { contentType } = attachment;
|
|
|
|
if (isUndefined(contentType)) {
|
|
return false;
|
|
}
|
|
|
|
if (isVisualMedia(attachment)) {
|
|
return false;
|
|
}
|
|
|
|
if (isVoiceMessage(attachment)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const isVoiceMessage = (attachment: Attachment): boolean => {
|
|
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
|
const hasFlag =
|
|
// tslint:disable-next-line no-bitwise
|
|
!isUndefined(attachment.flags) && (attachment.flags & flag) === flag;
|
|
if (hasFlag) {
|
|
return true;
|
|
}
|
|
|
|
const isLegacyAndroidVoiceMessage =
|
|
!isUndefined(attachment.contentType) &&
|
|
MIME.isAudio(attachment.contentType) &&
|
|
!attachment.fileName;
|
|
if (isLegacyAndroidVoiceMessage) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const save = ({
|
|
attachment,
|
|
document,
|
|
index,
|
|
timestamp,
|
|
}: {
|
|
attachment: AttachmentType;
|
|
document: Document;
|
|
index?: number;
|
|
getAbsolutePath: (relativePath: string) => string;
|
|
timestamp?: number;
|
|
}): void => {
|
|
const isObjectURLRequired = isUndefined(attachment.fileName);
|
|
const filename = getSuggestedFilename({ attachment, timestamp, index });
|
|
saveURLAsFile({ url: attachment.url, filename, document });
|
|
if (isObjectURLRequired) {
|
|
URL.revokeObjectURL(attachment.url);
|
|
}
|
|
};
|
|
|
|
export const getSuggestedFilename = ({
|
|
attachment,
|
|
timestamp,
|
|
index,
|
|
}: {
|
|
attachment: AttachmentType;
|
|
timestamp?: number | Date;
|
|
index?: number;
|
|
}): string => {
|
|
if (attachment.fileName?.length > 3) {
|
|
return attachment.fileName;
|
|
}
|
|
const prefix = 'session-attachment';
|
|
const suffix = timestamp ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') : '';
|
|
const fileType = getFileExtension(attachment);
|
|
const extension = fileType ? `.${fileType}` : '';
|
|
const indexSuffix = index ? `_${padStart(index.toString(), 3, '0')}` : '';
|
|
|
|
return `${prefix}${suffix}${indexSuffix}${extension}`;
|
|
};
|
|
|
|
export const getFileExtension = (attachment: AttachmentType): string | undefined => {
|
|
// we override textplain to the extension of the file
|
|
// for contenttype starting with application, the mimetype is probably wrong so just use the extension of the file instead
|
|
if (
|
|
!attachment.contentType ||
|
|
attachment.contentType === 'text/plain' ||
|
|
attachment.contentType.startsWith('application')
|
|
) {
|
|
if (attachment.fileName?.length) {
|
|
const dotLastIndex = attachment.fileName.lastIndexOf('.');
|
|
if (dotLastIndex !== -1) {
|
|
return attachment.fileName.substring(dotLastIndex + 1);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
switch (attachment.contentType) {
|
|
case 'video/quicktime':
|
|
return 'mov';
|
|
default:
|
|
return attachment.contentType.split('/')[1];
|
|
}
|
|
};
|