mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
fix attachments loading for avatar and exporting files
This commit is contained in:
parent
def03c8baa
commit
ed30be5334
25 changed files with 421 additions and 455 deletions
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export function getTimestamp(asInt = false) {
|
||||
const timestamp = Date.now() / 1000;
|
||||
return asInt ? Math.floor(timestamp) : timestamp;
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue