Skip to content

Commit da1b8ea

Browse files
committed
feat: Allow users to pause gifs
1 parent 6aa1822 commit da1b8ea

4 files changed

Lines changed: 134 additions & 3 deletions

File tree

src/script/components/Image/Image.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface BaseImageProps extends React.HTMLProps<HTMLDivElement> {
4848
interface RemoteDataImageProps extends BaseImageProps {
4949
image: AssetRemoteData;
5050
imageSizes?: {width: string; height: string; ratio: number};
51+
fileType: string;
5152
}
5253
interface AssetImageProps extends BaseImageProps {
5354
image: MediumImage;
@@ -56,12 +57,13 @@ interface AssetImageProps extends BaseImageProps {
5657
export const AssetImage = ({image, alt, ...props}: AssetImageProps) => {
5758
const {resource} = useKoSubscribableChildren(image, ['resource']);
5859

59-
return <Image image={resource} imageSizes={image} alt={alt} {...props} />;
60+
return <Image image={resource} imageSizes={image} fileType={image.file_type} alt={alt} {...props} />;
6061
};
6162

6263
export const Image = ({
6364
image,
6465
imageSizes,
66+
fileType,
6567
onClick,
6668
className,
6769
isQuote = false,
@@ -88,7 +90,7 @@ export const Image = ({
8890
'application/octet-stream', // Octet-stream is required to paste images from clipboard
8991
...Config.getConfig().ALLOWED_IMAGE_TYPES,
9092
];
91-
const url = await getAssetUrl(image, allowedImageTypes);
93+
const url = await getAssetUrl(image, allowedImageTypes, fileType);
9294
if (isUnmouted.current) {
9395
// Avoid re-rendering a component that is umounted
9496
return;
@@ -116,6 +118,49 @@ export const Image = ({
116118
const assetUrl = imageUrl?.url || dummyImageUrl;
117119
const isLoading = !imageUrl;
118120

121+
if (imageUrl?.pauseFrameUrl) {
122+
return (
123+
<InViewport
124+
onVisible={() => setIsInViewport(true)}
125+
css={getWrapperStyles(!!onClick)}
126+
className={cx(className, {'loading-dots image-asset--no-image': isLoading})}
127+
data-uie-status={isLoading ? 'loading' : 'loaded'}
128+
{...props}
129+
>
130+
<div className="image-asset--gif-wrapper">
131+
<img
132+
css={{...getImageStyle(imageSizes), ...imageStyles}}
133+
src={imageUrl?.pauseFrameUrl}
134+
role="presentation"
135+
alt={alt}
136+
data-uie-name={isLoading ? 'image-loader' : 'image-asset-img'}
137+
onClick={event => {
138+
if (!isLoading) {
139+
onClick?.(event);
140+
}
141+
}}
142+
/>
143+
<details open className="image-asset--gif">
144+
<summary role="button" aria-label="static image"></summary>
145+
<div className="image-asset--gif-animated">
146+
<img
147+
css={{...getImageStyle(imageSizes), ...imageStyles}}
148+
src={assetUrl}
149+
role="presentation"
150+
alt={alt}
151+
data-uie-name={isLoading ? 'image-loader' : 'image-asset-img'}
152+
onClick={event => {
153+
if (!isLoading) {
154+
onClick?.(event);
155+
}
156+
}}
157+
/>
158+
</div>
159+
</details>
160+
</div>
161+
</InViewport>
162+
);
163+
}
119164
return (
120165
<InViewport
121166
onVisible={() => setIsInViewport(true)}

src/script/components/MessagesList/Message/ContentMessage/asset/common/useAssetTransfer/useAssetTransfer.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import {useEffect, useState} from 'react';
2121

22+
import {GifUtil} from 'gifwrap';
2223
import {container} from 'tsyringe';
2324

2425
import {AssetRemoteData} from 'Repositories/assets/AssetRemoteData';
@@ -30,6 +31,7 @@ import {useKoSubscribableChildren} from 'Util/ComponentUtil';
3031

3132
export type AssetUrl = {
3233
url: string;
34+
pauseFrameUrl?: string;
3335
dispose: () => void;
3436
};
3537

@@ -59,7 +61,11 @@ export const useAssetTransfer = (message?: ContentMessage, assetRepository = con
5961
isPendingUpload: transferState === AssetTransferState.UPLOAD_PENDING,
6062
isUploaded: transferState === AssetTransferState.UPLOADED,
6163
isUploading: transferState === AssetTransferState.UPLOADING,
62-
getAssetUrl: async (resource: AssetRemoteData, acceptedMimeTypes?: string[]): Promise<AssetUrl> => {
64+
getAssetUrl: async (
65+
resource: AssetRemoteData,
66+
acceptedMimeTypes?: string[],
67+
fileType?: string,
68+
): Promise<AssetUrl> => {
6369
const blob = await assetRepository.load(resource);
6470
if (!blob) {
6571
throw new Error(`Asset could not be loaded`);
@@ -68,9 +74,45 @@ export const useAssetTransfer = (message?: ContentMessage, assetRepository = con
6874
throw new Error(`Mime type not accepted "${blob.type}"`);
6975
}
7076
const url = URL.createObjectURL(blob);
77+
78+
const pauseFrameUrl = await (async () => {
79+
if (blob.type == 'image/gif' || fileType == 'image/gif') {
80+
const gifArrayBuffer = await blob.arrayBuffer();
81+
const gifBuffer = Buffer.from(gifArrayBuffer);
82+
const frame0 = await GifUtil.read(gifBuffer).then(gifFile => gifFile.frames[0]);
83+
const canvas = document.createElement('canvas');
84+
const ctx = canvas.getContext('2d');
85+
if (ctx) {
86+
canvas.width = frame0.bitmap.width;
87+
canvas.height = frame0.bitmap.height;
88+
89+
const imageData = ctx.createImageData(frame0.bitmap.width, frame0.bitmap.height);
90+
imageData.data.set(frame0.bitmap.data);
91+
ctx.putImageData(imageData, 0, 0);
92+
}
93+
94+
const pauseImageBlob: Blob | null = await new Promise(resolve => {
95+
canvas.toBlob(
96+
blob => {
97+
resolve(blob);
98+
},
99+
'image/jpeg',
100+
0.9,
101+
);
102+
});
103+
104+
if (pauseImageBlob) {
105+
return URL.createObjectURL(pauseImageBlob);
106+
}
107+
return undefined;
108+
}
109+
return undefined;
110+
})();
111+
71112
return {
72113
dispose: () => URL.revokeObjectURL(url),
73114
url,
115+
pauseFrameUrl,
74116
};
75117
},
76118
transferState,

src/script/repositories/entity/message/FileAsset.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class FileAsset extends Asset {
4040
public readonly downloadProgress: ko.PureComputed<number | undefined>;
4141
public readonly cancelDownload: () => void;
4242
public file_size: number;
43+
public readonly file_type: string;
4344
public meta: Partial<AssetMetaData>;
4445
public readonly status: ko.Observable<AssetTransferState>;
4546
public readonly upload_failed_reason: ko.Observable<ProtobufAsset.NotUploaded>;

src/style/components/image.less

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,46 @@
4343
line-height: 1.3em;
4444
}
4545
}
46+
47+
.image-asset--gif-wrapper {
48+
position: relative;
49+
display: inline-block;
50+
51+
summary {
52+
position: absolute;
53+
z-index: 2;
54+
top: 0.5rem;
55+
right: 0.5rem;
56+
width: 0;
57+
height: 2em;
58+
border-width: 1em 0 1em 2em;
59+
border-style: solid;
60+
61+
border-color: transparent transparent transparent #202020;
62+
background: transparent;
63+
cursor: pointer;
64+
transition: 100ms all ease;
65+
}
66+
67+
[open] summary {
68+
border-width: 0 0 0 2em;
69+
border-style: double;
70+
}
71+
72+
/* for blink/webkit */
73+
details summary::-webkit-details-marker {
74+
display: none;
75+
}
76+
/* for firefox */
77+
details > summary:first-of-type {
78+
list-style: none;
79+
}
80+
81+
.image-asset--gif-animated img {
82+
position: absolute;
83+
top: 0px;
84+
left: 0px;
85+
display: inline-block;
86+
overflow: visible;
87+
}
88+
}

0 commit comments

Comments
 (0)