do not load right panel data unless it is visibl

This commit is contained in:
Audric Ackermann 2021-07-06 14:01:02 +10:00
parent f0db797a9a
commit c8aa73626e
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
23 changed files with 601 additions and 542 deletions

View File

@ -35,9 +35,9 @@ window.semver = semver;
window.platform = process.platform;
window.getTitle = () => title;
window.getEnvironment = () => config.environment;
window.isDev = () => config.environment === 'development';
window.getAppInstance = () => config.appInstance;
window.getVersion = () => config.version;
window.isDev = () => config.environment === 'development';
window.getExpiration = () => config.buildExpiration;
window.getCommitHash = () => config.commitHash;
window.getNodeVersion = () => config.node_version;

View File

@ -8,6 +8,7 @@ import { AttachmentType } from '../types/Attachment';
import { SessionInput } from './session/SessionInput';
import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton';
import { darkTheme, lightTheme } from '../state/ducks/SessionTheme';
import autoBind from 'auto-bind';
interface Props {
attachment: AttachmentType;
@ -31,8 +32,7 @@ export class CaptionEditor extends React.Component<Props, State> {
this.state = {
caption: caption || '',
};
this.onSave = this.onSave.bind(this);
this.onChange = this.onChange.bind(this);
autoBind(this);
this.inputRef = React.createRef();
}

View File

@ -5,9 +5,8 @@ import React, { useEffect, useState } from 'react';
import * as MIME from '../types/MIME';
import { Lightbox } from './Lightbox';
import { Message } from './conversation/media-gallery/types/Message';
import { AttachmentType } from '../types/Attachment';
import { AttachmentTypeWithPath } from '../types/Attachment';
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
@ -16,22 +15,16 @@ export interface MediaItemType {
thumbnailObjectUrl?: string;
contentType: MIME.MIMEType;
index: number;
attachment: AttachmentType;
message: Message;
attachment: AttachmentTypeWithPath;
messageTimestamp: number;
messageSender: string;
messageId: string;
}
type Props = {
close: () => void;
media: Array<MediaItemType>;
onSave?: (options: {
attachment: AttachmentType;
message: Message;
index: number;
messageTimestamp?: number;
messageSender: string;
}) => void;
onSave?: (saveData: MediaItemType) => void;
selectedIndex: number;
};
@ -66,16 +59,10 @@ export const LightboxGallery = (props: Props) => {
}
const mediaItem = media[currentIndex];
onSave({
attachment: mediaItem.attachment,
message: mediaItem.message,
index: mediaItem.index,
messageTimestamp: mediaItem.messageTimestamp || mediaItem?.message?.sent_at,
messageSender: mediaItem.messageSender || (mediaItem?.message as any)?.source,
});
onSave(mediaItem);
};
const objectURL = selectedMedia.objectURL || 'images/alert-outline.svg';
const objectURL = selectedMedia?.objectURL || 'images/alert-outline.svg';
const { attachment } = selectedMedia;
const saveCallback = onSave ? handleSave : undefined;

View File

@ -66,7 +66,7 @@ const OnionPathModalInner = () => {
<OnionNodeStatusLight
glowDuration={glowDuration}
glowStartDelay={index}
key={index}
key={`light-${index}`}
/>
);
})}
@ -80,7 +80,11 @@ const OnionPathModalInner = () => {
if (!labelText) {
labelText = window.i18n('unknownCountry');
}
return labelText ? <div className="onion__node__country">{labelText}</div> : null;
return labelText ? (
<div className="onion__node__country" key={`country-${index}`}>
{labelText}
</div>
) : null;
})}
</Flex>
</Flex>

View File

@ -30,23 +30,23 @@ export class AttachmentSection extends React.Component<Props> {
return mediaItems.map((mediaItem, position, array) => {
const shouldShowSeparator = position < array.length - 1;
const { message, index, attachment } = mediaItem;
const { index, attachment, messageTimestamp, messageId } = mediaItem;
const onClick = this.createClickHandler(mediaItem);
switch (type) {
case 'media':
return (
<MediaGridItem key={`${message.id}-${index}`} mediaItem={mediaItem} onClick={onClick} />
<MediaGridItem key={`${messageId}-${index}`} mediaItem={mediaItem} onClick={onClick} />
);
case 'documents':
return (
<DocumentListItem
key={`${message.id}-${index}`}
key={`${messageId}-${index}`}
fileName={attachment.fileName}
fileSize={attachment.size}
shouldShowSeparator={shouldShowSeparator}
onClick={onClick}
timestamp={message.received_at}
timestamp={messageTimestamp}
/>
);
default:
@ -57,12 +57,11 @@ export class AttachmentSection extends React.Component<Props> {
private readonly createClickHandler = (mediaItem: MediaItemType) => () => {
const { onItemClick, type } = this.props;
const { message, attachment } = mediaItem;
if (!onItemClick) {
return;
}
onItemClick({ type, message, attachment });
onItemClick({ mediaItem, type });
};
}

View File

@ -5,46 +5,30 @@ import moment from 'moment';
// tslint:disable-next-line:match-default-export-name
import formatFileSize from 'filesize';
interface Props {
type Props = {
// Required
timestamp: number;
// Optional
fileName?: string;
fileSize?: number;
fileSize?: number | null;
onClick?: () => void;
shouldShowSeparator?: boolean;
}
};
export class DocumentListItem extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
shouldShowSeparator: true,
};
export const DocumentListItem = (props: Props) => {
const { shouldShowSeparator, fileName, fileSize, timestamp } = props;
public render() {
const { shouldShowSeparator } = this.props;
const defaultShowSeparator = shouldShowSeparator === undefined ? true : shouldShowSeparator;
return (
<div
className={classNames(
'module-document-list-item',
shouldShowSeparator ? 'module-document-list-item--with-separator' : null
)}
>
{this.renderContent()}
</div>
);
}
private renderContent() {
const { fileName, fileSize, timestamp } = this.props;
return (
<div
className="module-document-list-item__content"
role="button"
onClick={this.props.onClick}
>
return (
<div
className={classNames(
'module-document-list-item',
defaultShowSeparator ? 'module-document-list-item--with-separator' : null
)}
>
<div className="module-document-list-item__content" role="button" onClick={props.onClick}>
<div className="module-document-list-item__icon" />
<div className="module-document-list-item__metadata">
<span className="module-document-list-item__file-name">{fileName}</span>
@ -56,6 +40,6 @@ export class DocumentListItem extends React.Component<Props> {
{moment(timestamp).format('ddd, MMM D, Y')}
</div>
</div>
);
}
}
</div>
);
};

View File

@ -25,9 +25,9 @@ export const groupMediaItemsByDate = (
const referenceDateTime = moment.utc(timestamp);
const sortedMediaItem = sortBy(mediaItems, mediaItem => {
const { message } = mediaItem;
const { messageTimestamp } = mediaItem;
return -message.received_at;
return -messageTimestamp;
});
const messagesWithSection = sortedMediaItem.map(withSection(referenceDateTime));
const groupedMediaItem = groupBy(messagesWithSection, 'type');
@ -102,8 +102,8 @@ const withSection = (referenceDateTime: moment.Moment) => (
const thisWeek = moment(referenceDateTime).startOf('isoWeek');
const thisMonth = moment(referenceDateTime).startOf('month');
const { message } = mediaItem;
const mediaItemReceivedDate = moment.utc(message.received_at);
const { messageTimestamp } = mediaItem;
const mediaItemReceivedDate = moment.utc(messageTimestamp);
if (mediaItemReceivedDate.isAfter(today)) {
return {
order: 0,

View File

@ -1,8 +1,6 @@
import { AttachmentType } from '../../../../types/Attachment';
import { Message } from './Message';
import { MediaItemType } from '../../../LightboxGallery';
export interface ItemClickEvent {
message: Message;
attachment: AttachmentType;
mediaItem: MediaItemType;
type: 'media' | 'documents';
}

View File

@ -592,6 +592,9 @@ export class SessionCompositionBox extends React.Component<Props, State> {
...ret.image,
url: URL.createObjectURL(blob),
fileName: 'preview',
fileSize: null,
screenshot: null,
thumbnail: null,
};
image = imageAttachment;
}

View File

@ -17,11 +17,11 @@ import { SessionMessagesList } from './SessionMessagesList';
import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
import { Message } from '../../conversation/media-gallery/types/Message';
import { AttachmentType, save } from '../../../types/Attachment';
import { AttachmentType, AttachmentTypeWithPath, save } from '../../../types/Attachment';
import { ToastUtils, UserUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone';
import { ConversationType } from '../../../state/ducks/conversations';
import { ConversationType, PropsForMessage } from '../../../state/ducks/conversations';
import { MessageView } from '../../MainViewController';
import { pushUnblockToSend } from '../../../session/utils/Toast';
import { MessageDetail } from '../../conversation/MessageDetail';
@ -39,6 +39,7 @@ import { updateMentionsMembers } from '../../../state/ducks/mentionsInput';
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { SessionButtonColor } from '../SessionButton';
import { usingClosedConversationDetails } from '../usingClosedConversationDetails';
interface State {
// Message sending progress
messageProgressVisible: boolean;
@ -74,7 +75,7 @@ interface State {
export interface LightBoxOptions {
media: Array<MediaItemType>;
attachment: any;
attachment: AttachmentTypeWithPath;
}
interface Props {
@ -249,7 +250,6 @@ export class SessionConversation extends React.Component<Props, State> {
return (
<SessionTheme theme={this.props.theme}>
<div className="conversation-header">{this.renderHeader()}</div>
{/* <SessionProgress
visible={this.state.messageProgressVisible}
value={this.state.sendingProgress}
@ -257,7 +257,6 @@ export class SessionConversation extends React.Component<Props, State> {
sendStatus={this.state.sendingProgressStatus}
resetProgress={this.resetSendingProgress}
/> */}
<div
// if you change the classname, also update it on onKeyDown
className={classNames('conversation-content', selectionMode && 'selection-mode')}
@ -306,10 +305,13 @@ export class SessionConversation extends React.Component<Props, State> {
theme={this.props.theme}
/>
</div>
<div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}>
<SessionRightPanelWithDetails {...this.getRightPanelProps()} />
<SessionRightPanelWithDetails
{...this.getRightPanelProps()}
isShowing={showOptionsPane}
/>
</div>
)
</SessionTheme>
);
}
@ -457,7 +459,7 @@ export class SessionConversation extends React.Component<Props, State> {
phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(),
avatarPath: conversation.getAvatarPath(),
isKickedFromGroup: conversation.get('isKickedFromGroup'),
isKickedFromGroup: Boolean(conversation.get('isKickedFromGroup')),
left: conversation.get('left'),
isGroup: !conversation.isPrivate(),
isPublic: conversation.isPublic(),
@ -714,20 +716,25 @@ export class SessionConversation extends React.Component<Props, State> {
});
}
private onClickAttachment(attachment: any, message: any) {
// message is MessageModelProps.propsForMessage I think
const media = (message.attachments || []).map((attachmentForMedia: any) => {
private onClickAttachment(attachment: AttachmentTypeWithPath, propsForMessage: PropsForMessage) {
let index = -1;
const media = (propsForMessage.attachments || []).map(attachmentForMedia => {
index++;
const messageTimestamp =
propsForMessage.timestamp || propsForMessage.serverTimestamp || propsForMessage.receivedAt;
return {
objectURL: attachmentForMedia.url,
index: _.clone(index),
objectURL: attachmentForMedia.url || undefined,
contentType: attachmentForMedia.contentType,
attachment: attachmentForMedia,
messageSender: message.authorPhoneNumber,
messageTimestamp: message.direction !== 'outgoing' ? message.timestamp : undefined, // do not set this field when the message was sent from us
// if it is set, this will trigger a sending of DataExtractionNotification to that user, but for an attachment we sent ourself.
messageSender: propsForMessage.authorPhoneNumber,
messageTimestamp,
messageId: propsForMessage.id,
};
});
const lightBoxOptions: LightBoxOptions = {
media,
media: media as any,
attachment,
};
this.setState({ lightBoxOptions });
@ -820,8 +827,9 @@ export class SessionConversation extends React.Component<Props, State> {
private renderLightBox({ media, attachment }: LightBoxOptions) {
const selectedIndex =
media.length > 1
? media.findIndex((mediaMessage: any) => mediaMessage.attachment.path === attachment.path)
? media.findIndex(mediaMessage => mediaMessage.attachment.path === attachment.path)
: 0;
console.warn('renderLightBox', { media, attachment });
return (
<LightboxGallery
media={media}
@ -837,15 +845,11 @@ export class SessionConversation extends React.Component<Props, State> {
// THIS DOES NOT DOWNLOAD ANYTHING! it just saves it where the user wants
private async saveAttachment({
attachment,
message,
index,
messageTimestamp,
messageSender,
}: {
attachment: AttachmentType;
message?: Message;
index?: number;
messageTimestamp?: number;
messageTimestamp: number;
messageSender: string;
}) {
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
@ -854,7 +858,7 @@ export class SessionConversation extends React.Component<Props, State> {
attachment,
document,
getAbsolutePath: getAbsoluteAttachmentPath,
timestamp: messageTimestamp || message?.received_at,
timestamp: messageTimestamp,
});
await sendDataExtractionNotification(
@ -937,6 +941,9 @@ export class SessionConversation extends React.Component<Props, State> {
videoUrl: objectUrl,
url,
isVoiceMessage: false,
fileSize: null,
screenshot: null,
thumbnail: null,
},
]);
} catch (error) {
@ -958,6 +965,9 @@ export class SessionConversation extends React.Component<Props, State> {
contentType,
url: urlImage,
isVoiceMessage: false,
fileSize: null,
screenshot: null,
thumbnail: null,
},
]);
return;
@ -973,6 +983,9 @@ export class SessionConversation extends React.Component<Props, State> {
contentType,
url,
isVoiceMessage: false,
fileSize: null,
screenshot: null,
thumbnail: null,
},
]);
};
@ -1017,6 +1030,9 @@ export class SessionConversation extends React.Component<Props, State> {
fileName,
url: '',
isVoiceMessage: false,
fileSize: null,
screenshot: null,
thumbnail: null,
},
]);
}
@ -1033,6 +1049,9 @@ export class SessionConversation extends React.Component<Props, State> {
fileName,
isVoiceMessage: false,
url: '',
fileSize: null,
screenshot: null,
thumbnail: null,
},
]);
}

View File

@ -56,7 +56,7 @@ interface Props {
messageTimestamp,
}: {
attachment: any;
messageTimestamp?: number;
messageTimestamp: number;
messageSender: string;
}) => void;
onDeleteSelectedMessages: () => Promise<void>;
@ -336,12 +336,17 @@ export class SessionMessagesList extends React.Component<Props, State> {
};
regularProps.onClickAttachment = (attachment: AttachmentType) => {
this.props.onClickAttachment(attachment, messageProps);
this.props.onClickAttachment(attachment, messageProps.propsForMessage);
};
regularProps.onDownload = (attachment: AttachmentType) => {
const messageTimestamp =
messageProps.propsForMessage.timestamp ||
messageProps.propsForMessage.serverTimestamp ||
messageProps.propsForMessage.receivedAt ||
0;
this.props.onDownloadAttachment({
attachment,
messageTimestamp: messageProps.propsForMessage.timestamp,
messageTimestamp,
messageSender: messageProps.propsForMessage.authorPhoneNumber,
});
};

View File

@ -1,18 +1,18 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { Avatar, AvatarSize } from '../../Avatar';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton';
import { SessionDropdown } from '../SessionDropdown';
import { MediaGallery } from '../../conversation/media-gallery/MediaGallery';
import _ from 'lodash';
import _, { noop } from 'lodash';
import { TimerOption } from '../../conversation/ConversationHeader';
import { Constants } from '../../../session';
import {
ConversationAvatar,
usingClosedConversationDetails,
} from '../usingClosedConversationDetails';
import { save } from '../../../types/Attachment';
import { DefaultTheme, withTheme } from 'styled-components';
import { AttachmentTypeWithPath, save } from '../../../types/Attachment';
import { DefaultTheme, useTheme, withTheme } from 'styled-components';
import {
getMessagesWithFileAttachments,
getMessagesWithVisualMediaAttachments,
@ -32,345 +32,203 @@ import {
showUpdateGroupMembersByConvoId,
showUpdateGroupNameByConvoId,
} from '../../../interactions/conversationInteractions';
import { ItemClickEvent } from '../../conversation/media-gallery/types/ItemClickEvent';
import { MediaItemType } from '../../LightboxGallery';
// tslint:disable-next-line: no-submodule-imports
import useInterval from 'react-use/lib/useInterval';
interface Props {
type Props = {
id: string;
name?: string;
profileName?: string;
phoneNumber: string;
memberCount: number;
description: string;
avatarPath: string;
avatarPath: string | null;
timerOptions: Array<TimerOption>;
isPublic: boolean;
isAdmin: boolean;
isKickedFromGroup: boolean;
left: boolean;
isBlocked: boolean;
isShowing: boolean;
isGroup: boolean;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onGoBack: () => void;
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void;
theme: DefaultTheme;
}
};
interface State {
documents: Array<any>;
media: Array<any>;
async function getMediaGalleryProps(
conversationId: string,
medias: Array<MediaItemType>,
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void
): Promise<{
documents: Array<MediaItemType>;
media: Array<MediaItemType>;
onItemClick: any;
}
}> {
// 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 rawMedia = await getMessagesWithVisualMediaAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
});
const rawDocuments = await getMessagesWithFileAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
});
class SessionRightPanel extends React.Component<Props, State> {
public constructor(props: Props) {
super(props);
const media = _.flatten(
rawMedia.map(attributes => {
const { attachments, source, id, timestamp, serverTimestamp, received_at } = attributes;
this.state = {
documents: Array<any>(),
media: Array<any>(),
onItemClick: undefined,
};
}
return (attachments || [])
.filter(
(attachment: AttachmentTypeWithPath) =>
attachment.thumbnail && !attachment.pending && !attachment.error
)
.map((attachment: AttachmentTypeWithPath, index: number) => {
const { thumbnail } = attachment;
public componentWillMount() {
void this.getMediaGalleryProps().then(({ documents, media, onItemClick }) => {
this.setState({
documents,
media,
onItemClick,
});
});
}
public componentDidUpdate() {
const mediaScanInterval = 1000;
setTimeout(() => {
void this.getMediaGalleryProps().then(({ documents, media, onItemClick }) => {
const { documents: oldDocs, media: oldMedias } = this.state;
if (oldDocs.length !== documents.length || oldMedias.length !== media.length) {
this.setState({
documents,
media,
onItemClick,
});
}
});
}, mediaScanInterval);
}
public async getMediaGalleryProps(): Promise<{
documents: Array<any>;
media: Array<any>;
onItemClick: any;
}> {
// 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 conversationId = this.props.id;
const rawMedia = await getMessagesWithVisualMediaAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
});
const rawDocuments = await getMessagesWithFileAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
});
// First we upgrade these messages to ensure that they have thumbnails
const max = rawMedia.length;
for (let i = 0; i < max; i += 1) {
const message = rawMedia[i];
const { schemaVersion } = message;
if (schemaVersion < message.VERSION_NEEDED_FOR_DISPLAY) {
// Yep, we really do want to wait for each of these
// eslint-disable-next-line no-await-in-loop
rawMedia[i] = await window.Signal.Migrations.upgradeMessageSchema(message);
// eslint-disable-next-line no-await-in-loop
await rawMedia[i].commit();
}
}
const media = _.flatten(
rawMedia.map((message: { attachments: any }) => {
const { attachments } = message;
return (attachments || [])
.filter(
(attachment: { thumbnail: any; pending: any; error: any }) =>
attachment.thumbnail && !attachment.pending && !attachment.error
)
.map((attachment: { path?: any; contentType?: any; thumbnail?: any }, index: any) => {
const { thumbnail } = attachment;
return {
objectURL: window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail
? window.Signal.Migrations.getAbsoluteAttachmentPath(thumbnail.path)
: null,
contentType: attachment.contentType,
index,
attachment,
message,
};
});
})
);
// Unlike visual media, only one non-image attachment is supported
const documents = rawDocuments.map((message: { attachments: Array<any> }) => {
// this is to not fail if the attachment is invalid (could be a Long Attachment type which is not supported)
if (!message.attachments?.length) {
// window?.log?.info(
// 'Got a message with an empty list of attachment. Skipping...'
// );
return null;
}
const attachment = message.attachments[0];
return {
contentType: attachment.contentType,
index: 0,
attachment,
message,
};
});
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at as number | undefined;
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
await sendDataExtractionNotification(this.props.id, message?.source, timestamp);
};
const onItemClick = ({ message, attachment, type }: any) => {
switch (type) {
case 'documents': {
void saveAttachment({ message, attachment });
break;
}
case 'media': {
// don't set the messageTimestamp when we are the sender, so we don't trigger a notification when
// we save the same attachment we sent ourself to another user
const messageTimestamp =
message.source !== UserUtils.getOurPubKeyStrFromCache()
? message.sent_at || message.received_at
: undefined;
const lightBoxOptions = {
media,
const mediaItem: MediaItemType = {
objectURL: window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail
? window.Signal.Migrations.getAbsoluteAttachmentPath(thumbnail.path)
: null,
contentType: attachment.contentType || '',
index,
messageTimestamp: timestamp || serverTimestamp || received_at || 0,
messageSender: source,
messageId: id,
attachment,
messageTimestamp,
} as LightBoxOptions;
this.onShowLightBox(lightBoxOptions);
break;
}
};
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
return mediaItem;
});
})
);
// Unlike visual media, only one non-image attachment is supported
const documents = rawDocuments.map(attributes => {
// this is to not fail if the attachment is invalid (could be a Long Attachment type which is not supported)
if (!attributes.attachments?.length) {
// window?.log?.info(
// 'Got a message with an empty list of attachment. Skipping...'
// );
return null;
}
const attachment = attributes.attachments[0];
const { source, id, timestamp, serverTimestamp, received_at } = attributes;
return {
media,
documents: _.compact(documents), // remove null
onItemClick,
contentType: attachment.contentType,
index: 0,
attachment,
messageTimestamp: timestamp || serverTimestamp || received_at || 0,
messageSender: source,
messageId: id,
};
}
});
public onShowLightBox(lightboxOptions: LightBoxOptions) {
this.props.onShowLightBox(lightboxOptions);
}
// tslint:disable-next-line: cyclomatic-complexity
public render() {
const {
id,
memberCount,
name,
timerOptions,
isKickedFromGroup,
left,
isPublic,
isAdmin,
isBlocked,
isGroup,
} = this.props;
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0);
const commonNoShow = isKickedFromGroup || left || isBlocked;
const hasDisappearingMessages = !isPublic && !commonNoShow;
const leaveGroupString = isPublic
? window.i18n('leaveGroup')
: isKickedFromGroup
? window.i18n('youGotKickedFromGroup')
: left
? window.i18n('youLeftTheGroup')
: window.i18n('leaveGroup');
const disappearingMessagesOptions = timerOptions.map(option => {
return {
content: option.name,
onClick: async () => {
await setDisappearingMessagesByConvoId(id, option.value);
},
};
const saveAttachment = async ({
attachment,
messageTimestamp,
messageSender,
}: {
attachment: AttachmentTypeWithPath;
messageTimestamp: number;
messageSender: string;
}) => {
const timestamp = messageTimestamp;
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
await sendDataExtractionNotification(conversationId, messageSender, timestamp);
};
const showUpdateGroupNameButton = isAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
const onItemClick = (event: ItemClickEvent) => {
if (!event) {
console.warn('no event');
return;
}
const { mediaItem, type } = event;
switch (type) {
case 'documents': {
void saveAttachment({
messageSender: mediaItem.messageSender,
messageTimestamp: mediaItem.messageTimestamp,
attachment: mediaItem.attachment,
});
break;
}
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
const deleteConvoAction = isPublic
? () => {
deleteMessagesByConvoIdWithConfirmation(id);
}
: () => {
showLeaveGroupByConvoId(id);
case 'media': {
const lightBoxOptions: LightBoxOptions = {
media: medias,
attachment: mediaItem.attachment,
};
return (
<div className="group-settings">
{this.renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<>
<SpacerLG />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<SpacerLG />
</>
)}
<input className="description" placeholder={window.i18n('description')} />
{showUpdateGroupNameButton && (
<div
className="group-settings-item"
role="button"
onClick={async () => {
await showUpdateGroupNameByConvoId(id);
}}
>
{isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')}
</div>
)}
{showAddRemoveModeratorsButton && (
<>
<div
className="group-settings-item"
role="button"
onClick={() => {
showAddModeratorsByConvoId(id);
}}
>
{window.i18n('addModerators')}
</div>
<div
className="group-settings-item"
role="button"
onClick={() => {
showRemoveModeratorsByConvoId(id);
}}
>
{window.i18n('removeModerators')}
</div>
</>
)}
onShowLightBox(lightBoxOptions);
break;
}
{showUpdateGroupMembersButton && (
<div
className="group-settings-item"
role="button"
onClick={async () => {
await showUpdateGroupMembersByConvoId(id);
}}
>
{window.i18n('groupMembers')}
</div>
)}
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
{hasDisappearingMessages && (
<SessionDropdown
label={window.i18n('disappearingMessages')}
options={disappearingMessagesOptions}
/>
)}
return {
media,
documents: _.compact(documents), // remove null
onItemClick,
};
}
<MediaGallery documents={documents} media={media} onItemClick={onItemClick} />
{isGroup && (
// tslint:disable-next-line: use-simple-attributes
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
disabled={isKickedFromGroup || left}
buttonType={SessionButtonType.SquareOutline}
onClick={deleteConvoAction}
/>
)}
</div>
);
}
// tslint:disable: cyclomatic-complexity
// tslint:disable: max-func-body-length
export const SessionRightPanelWithDetails = (props: Props) => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
const [onItemClick, setOnItemClick] = useState<any>(undefined);
const theme = useTheme();
private renderHeader() {
const {
memberAvatars,
id,
onGoBack,
avatarPath,
isAdmin,
isPublic,
isKickedFromGroup,
isBlocked,
name,
profileName,
phoneNumber,
left,
} = this.props;
useEffect(() => {
let isRunning = true;
if (props.isShowing) {
void getMediaGalleryProps(props.id, media, props.onShowLightBox).then(results => {
console.warn('results2', results);
if (isRunning) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
}
});
}
return () => {
isRunning = false;
return;
};
}, [props.isShowing, props.id]);
useInterval(async () => {
if (props.isShowing) {
const results = await getMediaGalleryProps(props.id, media, props.onShowLightBox);
console.warn('results', results);
if (results.documents.length !== documents.length || results.media.length !== media.length) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
}
}
}, 10000);
function renderHeader() {
const { memberAvatars, onGoBack, avatarPath, profileName, phoneNumber } = props;
const showInviteContacts = (isPublic || isAdmin) && !isKickedFromGroup && !isBlocked && !left;
const userName = name || profileName || phoneNumber;
@ -382,10 +240,10 @@ class SessionRightPanel extends React.Component<Props, State> {
iconSize={SessionIconSize.Medium}
iconRotation={270}
onClick={onGoBack}
theme={this.props.theme}
theme={theme}
/>
<Avatar
avatarPath={avatarPath}
avatarPath={avatarPath || ''}
name={userName}
size={AvatarSize.XL}
memberAvatars={memberAvatars}
@ -397,17 +255,139 @@ class SessionRightPanel extends React.Component<Props, State> {
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={() => {
showInviteContactByConvoId(this.props.id);
showInviteContactByConvoId(props.id);
}}
theme={this.props.theme}
theme={theme}
/>
)}
</div>
</div>
);
}
}
export const SessionRightPanelWithDetails = usingClosedConversationDetails(
withTheme(SessionRightPanel)
);
const {
id,
memberCount,
name,
timerOptions,
isKickedFromGroup,
left,
isPublic,
isAdmin,
isBlocked,
isGroup,
} = props;
const showMemberCount = !!(memberCount && memberCount > 0);
const commonNoShow = isKickedFromGroup || left || isBlocked;
console.warn('AUDRIC: render right panel');
const hasDisappearingMessages = !isPublic && !commonNoShow;
const leaveGroupString = isPublic
? window.i18n('leaveGroup')
: isKickedFromGroup
? window.i18n('youGotKickedFromGroup')
: left
? window.i18n('youLeftTheGroup')
: window.i18n('leaveGroup');
const disappearingMessagesOptions = timerOptions.map(option => {
return {
content: option.name,
onClick: async () => {
await setDisappearingMessagesByConvoId(id, option.value);
},
};
});
const showUpdateGroupNameButton = isAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
const deleteConvoAction = isPublic
? () => {
deleteMessagesByConvoIdWithConfirmation(id);
}
: () => {
showLeaveGroupByConvoId(id);
};
console.warn('onItemClick', onItemClick);
return (
<div className="group-settings">
{renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<>
<SpacerLG />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<SpacerLG />
</>
)}
{showUpdateGroupNameButton && (
<div
className="group-settings-item"
role="button"
onClick={async () => {
await showUpdateGroupNameByConvoId(id);
}}
>
{isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')}
</div>
)}
{showAddRemoveModeratorsButton && (
<>
<div
className="group-settings-item"
role="button"
onClick={() => {
showAddModeratorsByConvoId(id);
}}
>
{window.i18n('addModerators')}
</div>
<div
className="group-settings-item"
role="button"
onClick={() => {
showRemoveModeratorsByConvoId(id);
}}
>
{window.i18n('removeModerators')}
</div>
</>
)}
{showUpdateGroupMembersButton && (
<div
className="group-settings-item"
role="button"
onClick={async () => {
await showUpdateGroupMembersByConvoId(id);
}}
>
{window.i18n('groupMembers')}
</div>
)}
{hasDisappearingMessages && (
<SessionDropdown
label={window.i18n('disappearingMessages')}
options={disappearingMessagesOptions}
/>
)}
<MediaGallery documents={documents} media={media} onItemClick={onItemClick} />
{isGroup && (
// tslint:disable-next-line: use-simple-attributes
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
disabled={isKickedFromGroup || left}
buttonType={SessionButtonType.SquareOutline}
onClick={deleteConvoAction}
/>
)}
</div>
);
};

View File

@ -917,7 +917,7 @@ async function callChannel(name: string): Promise<any> {
export async function getMessagesWithVisualMediaAttachments(
conversationId: string,
options?: { limit: number }
): Promise<any> {
): Promise<Array<MessageAttributes>> {
return channels.getMessagesWithVisualMediaAttachments(conversationId, {
limit: options?.limit,
});
@ -926,7 +926,7 @@ export async function getMessagesWithVisualMediaAttachments(
export async function getMessagesWithFileAttachments(
conversationId: string,
options?: { limit: number }
): Promise<any> {
): Promise<Array<MessageAttributes>> {
return channels.getMessagesWithFileAttachments(conversationId, {
limit: options?.limit,
});

View File

@ -892,7 +892,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const unreadCount = await this.getUnreadCount();
this.set({ unreadCount });
await this.commit();
return model;
}
@ -971,12 +970,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
(m: any) => m.get('received_at') > newestUnreadDate
);
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
return !stillUnread.some(
(m: any) =>
m.propsForMessage &&
m.propsForMessage.text &&
m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1
);
return !stillUnread.some(m => m.getPropsForMessage()?.text?.indexOf(`@${ourNumber}`) !== -1);
})();
if (mentionRead) {

View File

@ -27,6 +27,7 @@ import {
LastMessageStatusType,
MessageModelProps,
MessagePropsDetails,
PropsForAttachment,
PropsForExpirationTimer,
PropsForGroupInvitation,
PropsForGroupUpdate,
@ -54,6 +55,7 @@ import { getV2OpenGroupRoom } from '../data/opengroups';
import { getMessageController } from '../session/messages';
import { isUsFromCache } from '../session/utils/User';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { AttachmentType, AttachmentTypeWithPath } from '../types/Attachment';
export class MessageModel extends Backbone.Model<MessageAttributes> {
constructor(attributes: MessageAttributesOptionals) {
@ -94,7 +96,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
propsForTimerNotification: this.getPropsForTimerNotification(),
};
perfEnd(`getPropsMessage-${this.id}`, 'getPropsMessage');
return messageProps;
}
@ -623,7 +624,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const previews = this.get('preview') || [];
return previews.map((preview: any) => {
let image = null;
let image: PropsForAttachment | null = null;
try {
if (preview.image) {
image = this.getPropsForAttachment(preview.image);
@ -667,29 +668,39 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
};
}
public getPropsForAttachment(attachment: {
path?: string;
pending?: boolean;
flags: number;
size: number;
screenshot: any;
thumbnail: any;
}) {
public getPropsForAttachment(attachment: AttachmentTypeWithPath): PropsForAttachment | null {
if (!attachment) {
return null;
}
const { path, pending, flags, size, screenshot, thumbnail } = attachment;
return {
...attachment,
fileSize: size ? filesize(size) : null,
isVoiceMessage:
flags &&
// eslint-disable-next-line no-bitwise
// tslint:disable-next-line: no-bitwise
flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
const {
id,
path,
contentType,
width,
height,
pending,
flags,
size,
screenshot,
thumbnail,
fileName,
} = attachment;
const isVoiceMessage =
// tslint:disable-next-line: no-bitwise
Boolean(flags && flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE) || false;
return {
id: id ? `${id}` : undefined,
contentType,
size: size || 0,
width: width || 0,
height: height || 0,
path,
fileName,
fileSize: size ? filesize(size) : null,
isVoiceMessage,
pending: Boolean(pending),
url: path ? window.Signal.Migrations.getAbsoluteAttachmentPath(path) : null,
screenshot: screenshot
? {
@ -1140,18 +1151,23 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
public isTrustedForAttachmentDownload() {
const senderConvoId = this.getSource();
const isClosedGroup = this.getConversation()?.isClosedGroup() || false;
if (!!this.get('isPublic') || isClosedGroup || isUsFromCache(senderConvoId)) {
return true;
}
// check the convo from this user
// we want the convo of the sender of this message
const senderConvo = getConversationController().get(senderConvoId);
if (!senderConvo) {
try {
const senderConvoId = this.getSource();
const isClosedGroup = this.getConversation()?.isClosedGroup() || false;
if (!!this.get('isPublic') || isClosedGroup || isUsFromCache(senderConvoId)) {
return true;
}
// check the convo from this user
// we want the convo of the sender of this message
const senderConvo = getConversationController().get(senderConvoId);
if (!senderConvo) {
return false;
}
return senderConvo.get('isTrustedForAttachmentDownload') || false;
} catch (e) {
window.log.warn('isTrustedForAttachmentDownload: error; ', e.message);
return false;
}
return senderConvo.get('isTrustedForAttachmentDownload') || false;
}
}
export class MessageCollection extends Backbone.Collection<MessageModel> {}

View File

@ -228,7 +228,6 @@ export interface MessageRegularProps {
authorProfileName?: string;
authorName?: string;
messageId?: string;
onClick: (data: any) => void;
referencedMessageNotFound: boolean;
};
previews: Array<any>;
@ -261,5 +260,5 @@ export interface MessageRegularProps {
playableMessageIndex?: number;
nextMessageToPlay?: number;
playNextMessage?: (value: number) => any;
playNextMessage?: (value: number) => void;
}

View File

@ -71,7 +71,7 @@ export function getUnpaddedAttachment(
export function addAttachmentPadding(data: ArrayBuffer): ArrayBuffer {
const originalUInt = new Uint8Array(data);
window?.log?.info('Adding attchment padding...');
window?.log?.info('Adding attachment padding...');
const paddedSize = Math.max(
541,

View File

@ -46,16 +46,10 @@ export class DataExtractionNotificationMessage extends ContentMessage {
export const sendDataExtractionNotification = async (
conversationId: string,
attachmentSender: string,
referencedAttachmentTimestamp?: number
referencedAttachmentTimestamp: number
) => {
const convo = getConversationController().get(conversationId);
if (
!convo ||
!convo.isPrivate() ||
convo.isMe() ||
UserUtils.isUsFromCache(PubKey.cast(attachmentSender)) ||
!referencedAttachmentTimestamp
) {
if (!convo || !convo.isPrivate() || convo.isMe() || UserUtils.isUsFromCache(attachmentSender)) {
window.log.warn('Not sending saving attachment notification for', attachmentSender);
return;
}

View File

@ -669,7 +669,8 @@ const sendOnionRequestHandlingSnodeEject = async ({
}): Promise<SnodeResponse> => {
// this sendOnionRequest() call has to be the only one like this.
// If you need to call it, call it through sendOnionRequestHandlingSnodeEject because this is the one handling path rebuilding and known errors
let response, decodingSymmetricKey;
let response;
let decodingSymmetricKey;
try {
// this might throw a timeout error
const result = await sendOnionRequest({

View File

@ -11,6 +11,7 @@ import {
MessageModelType,
PropsForDataExtractionNotification,
} from '../../models/messageType';
import { AttachmentType } from '../../types/Attachment';
export type MessageModelProps = {
propsForMessage: PropsForMessage;
@ -101,6 +102,34 @@ export type PropsForSearchResults = {
snippet?: string; //not sure about the type of snippet
};
export type PropsForAttachment = {
id?: string;
contentType: string;
size: number;
width?: number;
height?: number;
url: string;
path?: string;
fileSize: string | null;
isVoiceMessage: boolean;
pending: boolean;
fileName: string;
screenshot: {
contentType: string;
width: number;
height: number;
url?: string;
path?: string;
} | null;
thumbnail: {
contentType: string;
width: number;
height: number;
url?: string;
path?: string;
} | null;
};
export type PropsForMessage = {
text: string | null;
id: string;
@ -114,7 +143,7 @@ export type PropsForMessage = {
authorPhoneNumber: string;
conversationType: ConversationTypeEnum;
convoId: string;
attachments: any;
attachments: Array<PropsForAttachment>;
previews: any;
quote: any;
authorAvatarPath: string | null;

View File

@ -15,20 +15,20 @@ const generatedMessageTimestamp = Date.now();
const toMediaItem = (date: Date): MediaItemType => ({
objectURL: date.toUTCString(),
index: 0,
message: {
id: 'id',
received_at: date.getTime(),
sent_at: date.getTime(),
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
contentType: IMAGE_JPEG,
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
});
// tslint:disable: max-func-body-length
@ -62,37 +62,37 @@ describe('groupMediaItemsByDate', () => {
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
sent_at: 1523534400000,
received_at: 1523534400000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
{
objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1523491260000,
sent_at: 1523491260000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
],
},
@ -103,20 +103,19 @@ describe('groupMediaItemsByDate', () => {
objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1523491140000,
sent_at: 1523491140000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
],
},
@ -127,20 +126,19 @@ describe('groupMediaItemsByDate', () => {
objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1523232060000,
sent_at: 1523232060000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
],
},
@ -151,39 +149,37 @@ describe('groupMediaItemsByDate', () => {
objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1523231940000,
sent_at: 1523231940000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
{
objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
index: 0,
message: {
id: 'id',
received_at: 1522540860000,
sent_at: 1522540860000,
attachments: [],
},
contentType: IMAGE_JPEG,
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
],
},
@ -196,39 +192,37 @@ describe('groupMediaItemsByDate', () => {
objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1522540740000,
sent_at: 1522540740000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
{
objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1519912800000,
sent_at: 1519912800000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
],
},
@ -240,40 +234,38 @@ describe('groupMediaItemsByDate', () => {
{
objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT',
index: 0,
message: {
id: 'id',
received_at: 1298937540000,
sent_at: 1298937540000,
attachments: [],
},
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
contentType: IMAGE_JPEG,
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
messageId: '123456',
},
{
objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
index: 0,
contentType: IMAGE_JPEG,
message: {
id: 'id',
received_at: 1296554400000,
sent_at: 1296554400000,
attachments: [],
},
messageSender: generatedMessageSenderKey,
messageTimestamp: generatedMessageTimestamp,
attachment: {
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
fileSize: null,
screenshot: null,
thumbnail: null,
path: '123456',
id: 123456,
},
messageId: '123456',
},
],
},

View File

@ -15,6 +15,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.IMAGE_GIF,
fileSize: null,
screenshot: null,
thumbnail: null,
};
assert.strictEqual(Attachment.getFileExtension(input), 'gif');
});
@ -24,6 +27,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
fileSize: null,
screenshot: null,
thumbnail: null,
};
assert.strictEqual(Attachment.getFileExtension(input), 'mov');
});
@ -33,6 +39,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.odt',
url: 'funny-cat.odt',
contentType: MIME.ODT,
fileSize: null,
screenshot: null,
thumbnail: null,
};
assert.strictEqual(Attachment.getFileExtension(input), 'odt');
});
@ -45,6 +54,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
fileSize: null,
screenshot: null,
thumbnail: null,
};
const actual = Attachment.getSuggestedFilename({ attachment });
const expected = 'funny-cat.mov';
@ -55,6 +67,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
fileSize: null,
screenshot: null,
thumbnail: null,
};
const actual = Attachment.getSuggestedFilename({
attachment,
@ -68,6 +83,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.ini',
url: 'funny-cat.ini',
contentType: '',
fileSize: null,
screenshot: null,
thumbnail: null,
};
const actual = Attachment.getSuggestedFilename({
attachment,
@ -82,6 +100,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.txt',
url: 'funny-cat.txt',
contentType: 'text/plain',
fileSize: null,
screenshot: null,
thumbnail: null,
};
const actual = Attachment.getSuggestedFilename({
attachment,
@ -95,6 +116,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.json',
url: 'funny-cat.json',
contentType: '',
fileSize: null,
screenshot: null,
thumbnail: null,
};
const actual = Attachment.getSuggestedFilename({
attachment,
@ -110,6 +134,9 @@ describe('Attachment', () => {
contentType: MIME.VIDEO_QUICKTIME,
url: 'funny-cat.mov',
fileName: 'funny-cat.mov',
fileSize: null,
screenshot: null,
thumbnail: null,
};
const timestamp = moment('2000-01-01').toDate();
const actual = Attachment.getSuggestedFilename({
@ -126,6 +153,9 @@ describe('Attachment', () => {
fileName: '',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
fileSize: null,
screenshot: null,
thumbnail: null,
};
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
const actual = Attachment.getSuggestedFilename({
@ -142,6 +172,9 @@ describe('Attachment', () => {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
fileSize: null,
screenshot: null,
thumbnail: null,
};
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
const actual = Attachment.getSuggestedFilename({

View File

@ -26,22 +26,44 @@ export interface AttachmentType {
url: string;
videoUrl?: string;
size?: number;
fileSize?: string;
fileSize: string | null;
pending?: boolean;
width?: number;
height?: number;
screenshot?: {
screenshot: {
height: number;
width: number;
url: string;
contentType: MIME.MIMEType;
};
thumbnail?: {
} | 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