fix attachments loading for avatar and exporting files

This commit is contained in:
Audric Ackermann 2021-03-26 15:24:56 +11:00
parent def03c8baa
commit ed30be5334
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
25 changed files with 421 additions and 455 deletions

View file

@ -46,7 +46,7 @@ exports.createReader = root => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string");
}
console.time(`readFile: ${relativePath}`);
console.warn(`readFile: ${relativePath}`);
const absolutePath = path.join(root, relativePath);
const normalized = path.normalize(absolutePath);
if (!normalized.startsWith(root)) {

View file

@ -9,10 +9,9 @@ const dataURLToBlobSync = require('blueimp-canvas-to-blob');
const fse = require('fs-extra');
const { blobToArrayBuffer } = require('blob-util');
const {
arrayBufferToObjectURL,
} = require('../../../ts/util/arrayBufferToObjectURL');
const AttachmentTS = require('../../../ts/types/Attachment');
const DecryptedAttachmentsManager = require('../../../ts/session/crypto/DecryptedAttachmentsManager');
exports.blobToArrayBuffer = blobToArrayBuffer;
@ -30,16 +29,12 @@ exports.getImageDimensions = ({ objectUrl, logger }) =>
logger.error('getImageDimensions error', toLogFormat(error));
reject(error);
});
fse.readFile(objectUrl).then(buffer => {
AttachmentTS.decryptAttachmentBuffer(toArrayBuffer(buffer)).then(
decryptedData => {
//FIXME image/jpeg is hard coded
const srcData = `data:image/jpg;base64,${window.libsession.Utils.StringUtils.fromArrayBufferToBase64(
toArrayBuffer(decryptedData)
)}`;
image.src = srcData;
}
);
//FIXME image/jpeg is hard coded
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
objectUrl,
'image/jpg'
).then(decryptedUrl => {
image.src = decryptedUrl;
});
});
@ -85,16 +80,11 @@ exports.makeImageThumbnail = ({
reject(error);
});
fse.readFile(objectUrl).then(buffer => {
AttachmentTS.decryptAttachmentBuffer(toArrayBuffer(buffer)).then(
decryptedData => {
//FIXME image/jpeg is hard coded
const srcData = `data:image/jpg;base64,${window.libsession.Utils.StringUtils.fromArrayBufferToBase64(
toArrayBuffer(decryptedData)
)}`;
image.src = srcData;
}
);
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
objectUrl,
contentType
).then(decryptedUrl => {
image.src = decryptedUrl;
});
});
@ -128,45 +118,17 @@ exports.makeVideoScreenshot = ({
reject(error);
});
video.src = objectUrl;
video.muted = true;
// for some reason, this is to be started, otherwise the generated thumbnail will be empty
video.play();
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
objectUrl,
'image/jpg'
).then(decryptedUrl => {
video.src = decryptedUrl;
video.muted = true;
// for some reason, this is to be started, otherwise the generated thumbnail will be empty
video.play();
});
});
exports.makeVideoThumbnail = async ({
size,
videoObjectUrl,
logger,
contentType,
}) => {
let screenshotObjectUrl;
try {
const blob = await exports.makeVideoScreenshot({
objectUrl: videoObjectUrl,
contentType,
logger,
});
const data = await blobToArrayBuffer(blob);
screenshotObjectUrl = arrayBufferToObjectURL({
data,
type: contentType,
});
// We need to wait for this, otherwise the finally below will run first
const resultBlob = await exports.makeImageThumbnail({
size,
objectUrl: screenshotObjectUrl,
contentType,
logger,
});
return resultBlob;
} finally {
exports.revokeObjectUrl(screenshotObjectUrl);
}
};
exports.makeObjectUrl = (data, contentType) => {
const blob = new Blob([data], {
type: contentType,

View file

@ -1,145 +1,128 @@
import React from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder';
import { ConversationAvatar } from './session/usingClosedConversationDetails';
import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch';
interface Props {
export enum AvatarSize {
XS = 28,
S = 36,
M = 48,
L = 64,
XL = 80,
HUGE = 300,
}
type Props = {
avatarPath?: string;
name?: string; // display name, profileName or phoneNumber, whatever is set first
pubkey?: string;
size: number;
size: AvatarSize;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onAvatarClick?: () => void;
}
};
interface State {
imageBroken: boolean;
}
const Identicon = (props: Props) => {
const { size, name, pubkey } = props;
const userName = name || '0';
export class Avatar extends React.PureComponent<Props, State> {
public handleImageErrorBound: () => void;
public onAvatarClickBound: (e: any) => void;
return (
<AvatarPlaceHolder
diameter={size}
name={userName}
pubkey={pubkey}
colors={['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']}
borderColor={'#00000059'}
/>
);
};
public constructor(props: Props) {
super(props);
this.handleImageErrorBound = this.handleImageError.bind(this);
this.onAvatarClickBound = this.onAvatarClick.bind(this);
this.state = {
imageBroken: false,
};
const NoImage = (props: {
memberAvatars?: Array<ConversationAvatar>;
name?: string;
pubkey?: string;
size: AvatarSize;
}) => {
const { memberAvatars, size } = props;
// if no image but we have conversations set for the group, renders group members avatars
if (memberAvatars) {
return (
<ClosedGroupAvatar
size={size}
memberAvatars={memberAvatars}
i18n={window.i18n}
/>
);
}
public handleImageError() {
return <Identicon {...props} />;
};
const AvatarImage = (props: {
avatarPath?: string;
name?: string; // display name, profileName or phoneNumber, whatever is set first
imageBroken: boolean;
handleImageError: () => any;
}) => {
const { avatarPath, name, imageBroken, handleImageError } = props;
if (!avatarPath || imageBroken) {
return null;
}
return (
<img
onError={handleImageError}
alt={window.i18n('contactAvatarAlt', [name])}
src={avatarPath}
/>
);
};
export const Avatar = (props: Props) => {
const { avatarPath, size, memberAvatars, name } = props;
const [imageBroken, setImageBroken] = useState(false);
const handleImageError = () => {
window.log.warn(
'Avatar: Image failed to load; failing over to placeholder'
);
this.setState({
imageBroken: true,
});
}
setImageBroken(true);
};
public renderIdenticon() {
const { size, name, pubkey } = this.props;
// contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
const userName = name || '0';
const isClosedGroupAvatar = memberAvatars && memberAvatars.length;
const hasImage = urlToLoad && !imageBroken && !isClosedGroupAvatar;
return (
<AvatarPlaceHolder
diameter={size}
name={userName}
pubkey={pubkey}
colors={this.getAvatarColors()}
borderColor={this.getAvatarBorderColor()}
/>
);
}
const isClickable = !!props.onAvatarClick;
public renderImage() {
const { avatarPath, name } = this.props;
const { imageBroken } = this.state;
if (!avatarPath || imageBroken) {
return null;
}
return (
<img
onError={this.handleImageErrorBound}
alt={window.i18n('contactAvatarAlt', [name])}
src={avatarPath}
/>
);
}
public renderNoImage() {
const { memberAvatars, size } = this.props;
// if no image but we have conversations set for the group, renders group members avatars
if (memberAvatars) {
return (
<ClosedGroupAvatar
size={size}
memberAvatars={memberAvatars}
i18n={window.i18n}
return (
<div
className={classNames(
'module-avatar',
`module-avatar--${size}`,
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
isClickable && 'module-avatar-clickable'
)}
onClick={e => {
e.stopPropagation();
props.onAvatarClick?.();
}}
role="button"
>
{hasImage ? (
<AvatarImage
avatarPath={urlToLoad}
imageBroken={imageBroken}
name={name}
handleImageError={handleImageError}
/>
);
}
return this.renderIdenticon();
}
public render() {
const { avatarPath, size, memberAvatars } = this.props;
const { imageBroken } = this.state;
const isClosedGroupAvatar = memberAvatars && memberAvatars.length;
const hasImage = avatarPath && !imageBroken && !isClosedGroupAvatar;
if (
size !== 28 &&
size !== 36 &&
size !== 48 &&
size !== 64 &&
size !== 80 &&
size !== 300
) {
throw new Error(`Size ${size} is not supported!`);
}
const isClickable = !!this.props.onAvatarClick;
return (
<div
className={classNames(
'module-avatar',
`module-avatar--${size}`,
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
isClickable && 'module-avatar-clickable'
)}
onClick={e => {
this.onAvatarClickBound(e);
}}
role="button"
>
{hasImage ? this.renderImage() : this.renderNoImage()}
</div>
);
}
private onAvatarClick(e: any) {
if (this.props.onAvatarClick) {
e.stopPropagation();
this.props.onAvatarClick();
}
}
private getAvatarColors(): Array<string> {
// const theme = window.Events.getThemedSettings();
// defined in session-android as `profile_picture_placeholder_colors`
return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a'];
}
private getAvatarBorderColor(): string {
return '#00000059'; // borderAvatarColor in themes.scss
}
}
) : (
<NoImage {...props} />
)}
</div>
);
};

View file

@ -1,85 +1,47 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { getInitials } from '../../util/getInitials';
interface Props {
type Props = {
diameter: number;
name: string;
pubkey?: string;
colors: Array<string>;
borderColor: string;
}
};
interface State {
sha512Seed?: string;
}
const sha512FromPubkey = async (pubkey: string): Promise<string> => {
// tslint:disable-next-line: await-promise
const buf = await crypto.subtle.digest(
'SHA-512',
new TextEncoder().encode(pubkey)
);
export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
// tslint:disable: prefer-template restrict-plus-operands
return Array.prototype.map
.call(new Uint8Array(buf), (x: any) => ('00' + x.toString(16)).slice(-2))
.join('');
};
this.state = {
sha512Seed: undefined,
};
}
public componentDidMount() {
const { pubkey } = this.props;
if (pubkey) {
void this.sha512(pubkey).then((sha512Seed: string) => {
this.setState({ sha512Seed });
});
}
}
public componentDidUpdate(prevProps: Props, prevState: State) {
const { pubkey, name } = this.props;
if (pubkey === prevProps.pubkey && name === prevProps.name) {
export const AvatarPlaceHolder = (props: Props) => {
const { borderColor, colors, pubkey, diameter, name } = props;
const [sha512Seed, setSha512Seed] = useState(undefined as string | undefined);
useEffect(() => {
if (!pubkey) {
setSha512Seed(undefined);
return;
}
void sha512FromPubkey(pubkey).then(sha => {
setSha512Seed(sha);
});
}, [pubkey, name]);
if (pubkey) {
void this.sha512(pubkey).then((sha512Seed: string) => {
this.setState({ sha512Seed });
});
}
}
public render() {
const { borderColor, colors, diameter, name } = this.props;
const diameterWithoutBorder = diameter - 2;
const viewBox = `0 0 ${diameter} ${diameter}`;
const r = diameter / 2;
const rWithoutBorder = diameterWithoutBorder / 2;
if (!this.state.sha512Seed) {
// return grey circle
return (
<svg viewBox={viewBox}>
<g id="UrTavla">
<circle
cx={r}
cy={r}
r={rWithoutBorder}
fill="#d2d2d3"
shapeRendering="geometricPrecision"
stroke={borderColor}
strokeWidth="1"
/>
</g>
</svg>
);
}
const initial = getInitials(name)?.toLocaleUpperCase() || '0';
const fontSize = diameter * 0.5;
// Generate the seed simulate the .hashCode as Java
const hash = parseInt(this.state.sha512Seed.substring(0, 12), 16) || 0;
const bgColorIndex = hash % colors.length;
const bgColor = colors[bgColorIndex];
const diameterWithoutBorder = diameter - 2;
const viewBox = `0 0 ${diameter} ${diameter}`;
const r = diameter / 2;
const rWithoutBorder = diameterWithoutBorder / 2;
if (!sha512Seed) {
// return grey circle
return (
<svg viewBox={viewBox}>
<g id="UrTavla">
@ -87,38 +49,51 @@ export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
cx={r}
cy={r}
r={rWithoutBorder}
fill={bgColor}
fill="#d2d2d3"
shapeRendering="geometricPrecision"
stroke={borderColor}
strokeWidth="1"
/>
<text
fontSize={fontSize}
x="50%"
y="50%"
fill="white"
textAnchor="middle"
stroke="white"
strokeWidth={1}
alignmentBaseline="central"
>
{initial}
</text>
</g>
</svg>
);
}
private async sha512(str: string) {
// tslint:disable-next-line: await-promise
const buf = await crypto.subtle.digest(
'SHA-512',
new TextEncoder().encode(str)
);
const initial = getInitials(name)?.toLocaleUpperCase() || '0';
const fontSize = diameter * 0.5;
// tslint:disable: prefer-template restrict-plus-operands
return Array.prototype.map
.call(new Uint8Array(buf), (x: any) => ('00' + x.toString(16)).slice(-2))
.join('');
}
}
// Generate the seed simulate the .hashCode as Java
const hash = parseInt(sha512Seed.substring(0, 12), 16) || 0;
const bgColorIndex = hash % colors.length;
const bgColor = colors[bgColorIndex];
return (
<svg viewBox={viewBox}>
<g id="UrTavla">
<circle
cx={r}
cy={r}
r={rWithoutBorder}
fill={bgColor}
shapeRendering="geometricPrecision"
stroke={borderColor}
strokeWidth="1"
/>
<text
fontSize={fontSize}
x="50%"
y="50%"
fill="white"
textAnchor="middle"
stroke="white"
strokeWidth={1}
alignmentBaseline="central"
>
{initial}
</text>
</g>
</svg>
);
};

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { LocalizerType } from '../../types/Util';
import { ConversationAvatar } from '../session/usingClosedConversationDetails';
@ -10,19 +10,19 @@ interface Props {
}
export class ClosedGroupAvatar extends React.PureComponent<Props> {
public getClosedGroupAvatarsSize(size: number) {
public getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize {
// Always use the size directly under the one requested
switch (size) {
case 36:
return 28;
case 48:
return 36;
case 64:
return 48;
case 80:
return 64;
case 300:
return 80;
case AvatarSize.S:
return AvatarSize.XS;
case AvatarSize.M:
return AvatarSize.S;
case AvatarSize.L:
return AvatarSize.M;
case AvatarSize.XL:
return AvatarSize.L;
case AvatarSize.HUGE:
return AvatarSize.XL;
default:
throw new Error(
`Invalid size request for closed group avatar: ${size}`

View file

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { Emojify } from './conversation/Emojify';
import { LocalizerType } from '../types/Util';
@ -26,7 +26,7 @@ export class ContactListItem extends React.Component<Props> {
<Avatar
avatarPath={avatarPath}
name={userName}
size={36}
size={AvatarSize.S}
pubkey={phoneNumber}
/>
);

View file

@ -3,7 +3,7 @@ import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { contextMenu } from 'react-contexify';
import { Avatar } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { MessageBody } from './conversation/MessageBody';
import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
@ -69,7 +69,6 @@ class ConversationListItem extends React.PureComponent<Props> {
memberAvatars,
} = this.props;
const iconSize = 36;
const userName = name || profileName || phoneNumber;
return (
@ -77,7 +76,7 @@ class ConversationListItem extends React.PureComponent<Props> {
<Avatar
avatarPath={avatarPath}
name={userName}
size={iconSize}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={phoneNumber}
/>

View file

@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { QRCode } from 'react-qr-svg';
import { Avatar } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import {
SessionButton,
@ -262,7 +262,12 @@ export class EditProfileDialog extends React.Component<Props, State> {
const userName = profileName || pubkey;
return (
<Avatar avatarPath={avatar} name={userName} size={80} pubkey={pubkey} />
<Avatar
avatarPath={avatar}
name={userName}
size={AvatarSize.XL}
pubkey={pubkey}
/>
);
}

View file

@ -12,10 +12,10 @@ import {
SessionIconType,
} from './session/icon';
import { Flex } from './session/Flex';
import { DefaultTheme, useTheme } from 'styled-components';
import { DefaultTheme } from 'styled-components';
// useCss has some issues on our setup. so import it directly
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
import useUnmount from 'react-use/lib/useUnmount';
import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch';
import { darkTheme } from '../state/ducks/SessionTheme';
@ -208,23 +208,58 @@ export const LightboxObject = ({
contentType,
videoRef,
onObjectClick,
playVideo,
}: {
objectURL: string;
contentType: MIME.MIMEType;
videoRef: React.MutableRefObject<any>;
onObjectClick: (event: any) => any;
playVideo: () => void;
}) => {
const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType);
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
const playVideo = () => {
if (!videoRef) {
return;
}
const { current } = videoRef;
if (!current) {
return;
}
if (current.paused) {
void current.play();
} else {
current.pause();
}
};
const pauseVideo = () => {
if (!videoRef) {
return;
}
const { current } = videoRef;
if (current) {
current.pause();
}
};
// auto play video on showing a video attachment
useUnmount(() => {
pauseVideo();
});
if (isImageTypeSupported) {
return <img alt={window.i18n('lightboxImageAlt')} src={urlToLoad} />;
return <img style={styles.object} alt={window.i18n('lightboxImageAlt')} src={urlToLoad} />;
}
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
if (isVideoTypeSupported) {
if (urlToLoad) {
playVideo();
}
return (
<video
role="button"
@ -232,9 +267,9 @@ export const LightboxObject = ({
onClick={playVideo}
controls={true}
style={styles.object}
key={objectURL}
key={urlToLoad}
>
<source src={objectURL} />
<source src={urlToLoad} />
</video>
);
}
@ -264,23 +299,6 @@ export const Lightbox = (props: Props) => {
const theme = darkTheme;
const { caption, contentType, objectURL, onNext, onPrevious, onSave } = props;
const playVideo = () => {
if (!videoRef) {
return;
}
const { current } = videoRef;
if (!current) {
return;
}
if (current.paused) {
void current.play();
} else {
current.pause();
}
};
const onObjectClick = (event: any) => {
event.stopPropagation();
props.close?.();
@ -293,21 +311,16 @@ export const Lightbox = (props: Props) => {
props.close?.();
};
// auto play video on showing a video attachment
useEffect(() => {
playVideo();
}, []);
return (
<div
style={styles.container}
onClick={onContainerClick}
ref={containerRef}
role="dialog"
>
<div style={styles.container} role="dialog">
<div style={styles.mainContainer}>
<div style={styles.controlsOffsetPlaceholder} />
<div style={styles.objectParentContainer}>
<div
style={styles.objectParentContainer}
onClick={onContainerClick}
ref={containerRef}
role="button"
>
<div style={styles.objectContainer}>
{!is.undefined(contentType) ? (
<LightboxObject
@ -315,7 +328,6 @@ export const Lightbox = (props: Props) => {
contentType={contentType}
videoRef={videoRef}
onObjectClick={onObjectClick}
playVideo={playVideo}
/>
) : null}
{caption ? <div style={styles.caption}>{caption}</div> : null}

View file

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { MessageBodyHighlight } from './MessageBodyHighlight';
import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
@ -110,7 +110,7 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
<Avatar
avatarPath={from.avatarPath}
name={userName}
size={36}
size={AvatarSize.S}
pubkey={from.phoneNumber}
/>
);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Avatar } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { SessionModal } from './session/SessionModal';
import {
@ -63,7 +63,9 @@ export class UserDetailsDialog extends React.Component<Props, State> {
private renderAvatar() {
const { avatarPath, pubkey, profileName } = this.props;
const size = this.state.isEnlargedImageShown ? 300 : 80;
const size = this.state.isEnlargedImageShown
? AvatarSize.HUGE
: AvatarSize.XL;
const userName = profileName || pubkey;
return (

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import {
SessionIconButton,
@ -185,7 +185,7 @@ class ConversationHeaderInner extends React.Component<Props> {
<Avatar
avatarPath={avatarPath}
name={userName}
size={36}
size={AvatarSize.S}
onAvatarClick={() => {
this.onAvatarClick(phoneNumber);
}}

View file

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { Spinner } from '../Spinner';
import { MessageBody } from './MessageBody';
import { ImageGrid } from './ImageGrid';
@ -13,6 +13,42 @@ import { Quote } from './Quote';
import H5AudioPlayer from 'react-h5-audio-player';
// import 'react-h5-audio-player/lib/styles.css';
const AudioPlayerWithEncryptedFile = (props: {
src: string;
contentType: string;
}) => {
const theme = useTheme();
const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType);
return (
<H5AudioPlayer
src={urlToLoad}
layout="horizontal-reverse"
showSkipControls={false}
showJumpControls={false}
showDownloadProgress={false}
listenInterval={100}
customIcons={{
play: (
<SessionIcon
iconType={SessionIconType.Play}
iconSize={SessionIconSize.Small}
iconColor={theme.colors.textColorSubtle}
theme={theme}
/>
),
pause: (
<SessionIcon
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Small}
iconColor={theme.colors.textColorSubtle}
theme={theme}
/>
),
}}
/>
);
};
import {
canDisplayImage,
getExtensionForDisplay,
@ -34,12 +70,14 @@ import _ from 'lodash';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import uuid from 'uuid';
import { InView } from 'react-intersection-observer';
import { withTheme } from 'styled-components';
import { useTheme, withTheme } from 'styled-components';
import { MessageMetadata } from './message/MessageMetadata';
import { PubKey } from '../../session/types';
import { ToastUtils, UserUtils } from '../../session/utils';
import { ConversationController } from '../../session/conversations';
import { MessageRegularProps } from '../../models/messageType';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import src from 'redux-promise-middleware';
// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
@ -208,31 +246,9 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
e.stopPropagation();
}}
>
<H5AudioPlayer
<AudioPlayerWithEncryptedFile
src={firstAttachment.url}
layout="horizontal-reverse"
showSkipControls={false}
showJumpControls={false}
showDownloadProgress={false}
listenInterval={100}
customIcons={{
play: (
<SessionIcon
iconType={SessionIconType.Play}
iconSize={SessionIconSize.Small}
iconColor={this.props.theme.colors.textColorSubtle}
theme={this.props.theme}
/>
),
pause: (
<SessionIcon
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Small}
iconColor={this.props.theme.colors.textColorSubtle}
theme={this.props.theme}
/>
),
}}
contentType={firstAttachment.contentType}
/>
</div>
);
@ -499,7 +515,7 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
<Avatar
avatarPath={authorAvatarPath}
name={userName}
size={36}
size={AvatarSize.S}
onAvatarClick={() => {
onShowUserDetails(authorPhoneNumber);
}}

View file

@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { ContactName } from './ContactName';
import { Message } from './Message';
import { MessageRegularProps } from '../../models/messageType';
@ -39,7 +39,7 @@ export class MessageDetail extends React.Component<Props> {
<Avatar
avatarPath={avatarPath}
name={userName}
size={36}
size={AvatarSize.S}
pubkey={phoneNumber}
/>
);

View file

@ -3,7 +3,7 @@ import classNames from 'classnames';
import { SessionModal } from '../session/SessionModal';
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { DefaultTheme, withTheme } from 'styled-components';
interface Props {
@ -178,7 +178,7 @@ class UpdateGroupNameDialogInner extends React.Component<Props, State> {
<div className="avatar-center-inner">
<Avatar
avatarPath={avatarPath}
size={80}
size={AvatarSize.XL}
pubkey={this.props.pubkey}
/>
<div

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme';
import { SessionToastContainer } from './SessionToastContainer';
import { ConversationType } from '../../state/ducks/conversations';
@ -75,7 +75,7 @@ const Section = (props: { type: SectionType; avatarPath?: string }) => {
return (
<Avatar
avatarPath={avatarPath}
size={28}
size={AvatarSize.XS}
onAvatarClick={handleClick}
name={userName}
pubkey={ourNumber}

View file

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Avatar, AvatarSize } from '../Avatar';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { Constants } from '../../session';
import { DefaultTheme } from 'styled-components';
@ -92,7 +92,7 @@ class SessionMemberListItemInner extends React.Component<Props> {
<Avatar
avatarPath={authorAvatarPath}
name={userName}
size={28}
size={AvatarSize.XS}
pubkey={authorPhoneNumber}
/>
);

View file

@ -34,6 +34,7 @@ import {
getPubkeysInPublicConversation,
} from '../../../data/data';
import autoBind from 'auto-bind';
import { getDecryptedAttachmentUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
interface State {
// Message sending progress
@ -478,7 +479,7 @@ export class SessionConversation extends React.Component<Props, State> {
replyToMessage: this.replyToMessage,
showMessageDetails: this.showMessageDetails,
onClickAttachment: this.onClickAttachment,
onDownloadAttachment: this.downloadAttachment,
onDownloadAttachment: this.saveAttachment,
messageContainerRef: this.messageContainerRef,
onDeleteSelectedMessages: this.deleteSelectedMessages,
};
@ -923,13 +924,13 @@ export class SessionConversation extends React.Component<Props, State> {
this.setState({ lightBoxOptions: undefined });
}}
selectedIndex={selectedIndex}
onSave={this.downloadAttachment}
onSave={this.saveAttachment}
/>
);
}
// THIS DOES NOT DOWNLOAD ANYTHING! it just saves it where the user wants
private downloadAttachment({
private async saveAttachment({
attachment,
message,
index,
@ -939,7 +940,10 @@ export class SessionConversation extends React.Component<Props, State> {
index?: number;
}) {
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
attachment.url = await getDecryptedAttachmentUrl(
attachment.url,
attachment.contentType
);
save({
attachment,
document,

View file

@ -1,4 +0,0 @@
export function getTimestamp(asInt = false) {
const timestamp = Date.now() / 1000;
return asInt ? Math.floor(timestamp) : timestamp;
}

View file

@ -2,8 +2,6 @@ import React from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { getTimestamp } from './SessionConversationManager';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import {
SessionButton,
@ -57,6 +55,11 @@ interface State {
updateTimerInterval: NodeJS.Timeout;
}
function getTimestamp(asInt = false) {
const timestamp = Date.now() / 1000;
return asInt ? Math.floor(timestamp) : timestamp;
}
class SessionRecordingInner extends React.Component<Props, State> {
private readonly visualisationRef: React.RefObject<HTMLDivElement>;
private readonly visualisationCanvas: React.RefObject<HTMLCanvasElement>;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { Avatar } from '../../Avatar';
import { Avatar, AvatarSize } from '../../Avatar';
import {
SessionButton,
SessionButtonColor,
@ -21,6 +21,7 @@ import {
getMessagesWithFileAttachments,
getMessagesWithVisualMediaAttachments,
} from '../../../data/data';
import { getDecryptedAttachmentUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
interface Props {
id: string;
@ -159,8 +160,8 @@ class SessionRightPanel extends React.Component<Props, State> {
),
thumbnailObjectUrl: thumbnail
? window.Signal.Migrations.getAbsoluteAttachmentPath(
thumbnail.path
)
thumbnail.path
)
: null,
contentType: attachment.contentType,
index,
@ -193,21 +194,24 @@ class SessionRightPanel extends React.Component<Props, State> {
}
);
const saveAttachment = ({ attachment, message }: any = {}) => {
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at;
attachment.url =
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
attachment.url = await getDecryptedAttachmentUrl(
attachment.url,
attachment.contentType
);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
};
const onItemClick = ({ message, attachment, type }: any) => {
switch (type) {
case 'documents': {
saveAttachment({ message, attachment });
void saveAttachment({ message, attachment });
break;
}
@ -259,10 +263,10 @@ class SessionRightPanel extends React.Component<Props, State> {
const leaveGroupString = isPublic
? window.i18n('leaveGroup')
: isKickedFromGroup
? window.i18n('youGotKickedFromGroup')
: left
? window.i18n('youLeftTheGroup')
: window.i18n('leaveGroup');
? window.i18n('youGotKickedFromGroup')
: left
? window.i18n('youLeftTheGroup')
: window.i18n('leaveGroup');
const disappearingMessagesOptions = timerOptions.map(option => {
return {
@ -391,7 +395,7 @@ class SessionRightPanel extends React.Component<Props, State> {
<Avatar
avatarPath={avatarPath}
name={userName}
size={80}
size={AvatarSize.XL}
memberAvatars={memberAvatars}
pubkey={id}
/>

View file

@ -1,9 +1,6 @@
import { useEffect, useState } from 'react';
import toArrayBuffer from 'to-arraybuffer';
import * as fse from 'fs-extra';
import { decryptAttachmentBuffer } from '../types/Attachment';
const urlToDecryptedBlobMap = new Map<string, string>();
import { getDecryptedAttachmentUrl } from '../session/crypto/DecryptedAttachmentsManager';
export const useEncryptedFileFetch = (url: string, contentType: string) => {
// tslint:disable-next-line: no-bitwise
@ -11,42 +8,8 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
const [loading, setLoading] = useState(true);
async function fetchUrl() {
if (url.startsWith('blob:')) {
setUrlToLoad(url);
} else if (
window.Signal.Migrations.attachmentsPath &&
url.startsWith(window.Signal.Migrations.attachmentsPath)
) {
// this is a file encoded by session on our current attachments path.
// we consider the file is encrypted.
// if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it
if (urlToDecryptedBlobMap.has(url)) {
// typescript does not realize that the has above makes sure the get is not undefined
setUrlToLoad(urlToDecryptedBlobMap.get(url) as string);
} else {
const encryptedFileContent = await fse.readFile(url);
const decryptedContent = await decryptAttachmentBuffer(
toArrayBuffer(encryptedFileContent)
);
if (decryptedContent?.length) {
const arrayBuffer = decryptedContent.buffer;
const { makeObjectUrl } = window.Signal.Types.VisualAttachment;
const obj = makeObjectUrl(arrayBuffer, contentType);
if (!urlToDecryptedBlobMap.has(url)) {
urlToDecryptedBlobMap.set(url, obj);
}
setUrlToLoad(obj);
} else {
// failed to decrypt, fallback to url image loading
setUrlToLoad(url);
}
}
} else {
// already a blob.
setUrlToLoad(url);
}
const decryptedUrl = await getDecryptedAttachmentUrl(url, contentType);
setUrlToLoad(decryptedUrl);
setLoading(false);
}

View file

@ -1,5 +1,59 @@
/**
* This file handles attachments for us.
* If the attachment filepath is an encrypted one. It will decrypt it, cache it, and return the blob url to it.
*/
import toArrayBuffer from 'to-arraybuffer';
import * as fse from 'fs-extra';
import { decryptAttachmentBuffer } from '../../types/Attachment';
// FIXME.
// add a way to clean those from time to time (like every hours?)
// add a way to remove the blob when the attachment file path is removed (message removed?)
const urlToDecryptedBlobMap = new Map<string, string>();
export const getDecryptedAttachmentUrl = async (
url: string,
contentType: string
): Promise<string> => {
if (!url) {
return url;
}
if (url.startsWith('blob:')) {
return url;
} else if (
window.Signal.Migrations.attachmentsPath &&
url.startsWith(window.Signal.Migrations.attachmentsPath)
) {
// this is a file encoded by session on our current attachments path.
// we consider the file is encrypted.
// if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it
console.warn('url:', url, ' has:', urlToDecryptedBlobMap.has(url));
if (urlToDecryptedBlobMap.has(url)) {
// typescript does not realize that the has above makes sure the get is not undefined
return urlToDecryptedBlobMap.get(url) as string;
} else {
const encryptedFileContent = await fse.readFile(url);
const decryptedContent = await decryptAttachmentBuffer(
toArrayBuffer(encryptedFileContent)
);
if (decryptedContent?.length) {
const arrayBuffer = decryptedContent.buffer;
const { makeObjectUrl } = window.Signal.Types.VisualAttachment;
const obj = makeObjectUrl(arrayBuffer, contentType);
console.warn('makeObjectUrl: ', obj, contentType);
export const getDecryptedUrl
if (!urlToDecryptedBlobMap.has(url)) {
urlToDecryptedBlobMap.set(url, obj);
}
return obj;
} else {
// failed to decrypt, fallback to url image loading
return url;
}
}
} else {
// Not sure what we got here. Just return the file.
return url;
}
};

View file

@ -1,6 +1,7 @@
import * as MessageEncrypter from './MessageEncrypter';
import * as DecryptedAttachmentsManager from './DecryptedAttachmentsManager';
export { MessageEncrypter };
export { MessageEncrypter, DecryptedAttachmentsManager };
// libsodium-wrappers requires the `require` call to work
// tslint:disable-next-line: no-require-imports

View file

@ -418,15 +418,11 @@ export const getFileExtension = (
return attachment.contentType.split('/')[1];
}
};
let indexEncrypt = 0;
export const encryptAttachmentBuffer = async (bufferIn: ArrayBuffer) => {
if (!isArrayBuffer(bufferIn)) {
throw new TypeError("'bufferIn' must be an array buffer");
}
const ourIndex = indexEncrypt;
indexEncrypt++;
console.time(`timer #*. encryptAttachmentBuffer ${ourIndex}`);
const uintArrayIn = new Uint8Array(bufferIn);
const sodium = await getSodium();
@ -457,13 +453,10 @@ export const encryptAttachmentBuffer = async (bufferIn: ArrayBuffer) => {
);
encryptedBufferWithHeader.set(header);
encryptedBufferWithHeader.set(bufferOut, header.length);
console.timeEnd(`timer #*. encryptAttachmentBuffer ${ourIndex}`);
return { encryptedBufferWithHeader, header, key };
};
let indexDecrypt = 0;
export const decryptAttachmentBuffer = async (
bufferIn: ArrayBuffer,
key: string = '0c5f7147b6d3239cbb5a418814cee1bfca2df5c94bffddf22ee37eea3ede972b'
@ -471,9 +464,6 @@ export const decryptAttachmentBuffer = async (
if (!isArrayBuffer(bufferIn)) {
throw new TypeError("'bufferIn' must be an array buffer");
}
const ourIndex = indexDecrypt;
indexDecrypt++;
console.time(`timer .*# decryptAttachmentBuffer ${ourIndex}`);
const sodium = await getSodium();
const header = new Uint8Array(
@ -494,7 +484,6 @@ export const decryptAttachmentBuffer = async (
state,
encryptedBuffer
);
console.timeEnd(`timer .*# decryptAttachmentBuffer ${ourIndex}`);
// we expect the final tag to be there. If not, we might have an issue with this file
// maybe not encrypted locally?
if (
@ -503,8 +492,6 @@ export const decryptAttachmentBuffer = async (
return messageTag.message;
}
} catch (e) {
console.timeEnd(`timer .*# decryptAttachmentBuffer ${ourIndex}`);
window.log.warn('Failed to load the file as an encrypted one', e);
}
return new Uint8Array();