Skip to content

Conversation

@akshaymankar
Copy link
Member

@akshaymankar akshaymankar commented Dec 18, 2025

Pull Request

Its very jarring when a gif keeps looping while writing messages, so I thought I'll implement a play/pause button.

Somethings to note:

  • The pause button, actually shows the first frame of the gif, so its more of a stop button.
  • I tried to make the buttons only appear on hover, but I suck at CSS, so please help.
  • There are no tests, I can try to add them (after my vacation).
  • Further enahancement: extend the feature to support animated PNG and animated webp images.

Inspired by:

Summary

  • What did I change and why?
    The FileAsset type now exposes file_type which is then thread through various types to decide whether to show gif logic.

  • Risks and how to roll out / roll back (e.g. feature flags):

I haven't tested this with large GIFs, there rendering could be slower


Security Checklist (required)

  • External inputs are validated & sanitized on client and/or server where applicable.
  • API responses are validated; unexpected shapes are handled safely (fallbacks or errors).
  • No unsafe HTML is rendered; if unavoidable, sanitization is applied and documented where it happens.
  • Injection risks (XSS/SQL/command) are prevented via safe APIs and/or escaping.

Standards Acknowledgement (required)


Screenshots or demo (if the user interface changed)

Notes for reviewers

  • Trade-offs:
  • Follow-ups (linked issues):
  • Linked PRs (e.g. web-packages):

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a play/pause control for GIF images to reduce distraction from looping animations. The feature extracts the first frame of each GIF to display when paused, using a details/summary HTML element for the toggle control.

Key Changes:

  • Exposes file_type property in FileAsset to identify GIF images
  • Implements GIF frame extraction using the gifwrap library to create static preview images
  • Adds play/pause UI control using HTML details/summary elements with custom CSS styling

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 15 comments.

File Description
src/script/repositories/entity/message/FileAsset.ts Adds readonly file_type property to track image MIME types
src/script/components/MessagesList/Message/ContentMessage/asset/common/useAssetTransfer/useAssetTransfer.ts Implements GIF first-frame extraction logic and returns pauseFrameUrl alongside main asset URL
src/script/components/Image/Image.tsx Adds conditional rendering for GIF images with play/pause controls using details/summary elements
src/style/components/image.less Defines CSS for GIF wrapper, play/pause button triangle shapes, and animated GIF layering

Review Summary: Changes Requested

The PR implements an interesting feature but has several issues that should be addressed before merging:

Critical Issues (2):

  • Missing error handling for GIF processing that could crash the app
  • Memory leak from unreleased pauseFrameUrl object URLs

Important Issues (7):

  • Behavioral inconsistency (starts playing by default despite PR motivation)
  • Missing accessibility features (unclear labels, keyboard support concerns)
  • Type safety issue with optional fileType
  • Performance concerns with large GIF processing
  • Insufficient error handling for canvas context
  • Missing test coverage
  • Hardcoded colors that may not respect themes

Minor Issues (4):

  • Code duplication in rendering logic
  • Naming clarity improvements needed
  • CSS best practices (zero units, hover states)
  • Use of loose equality operator

Most critical is addressing the error handling and memory leak to prevent runtime failures. The accessibility and behavioral issues should also be resolved to ensure the feature works as intended for all users.

}
}}
/>
<details open className="image-asset--gif">
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details element starts in an "open" state, meaning the animated GIF plays by default. This contradicts the PR description stating that GIFs looping while writing messages is jarring. Consider removing the "open" attribute to start in a paused state by default, allowing users to opt-in to the animation.

Suggested change
<details open className="image-asset--gif">
<details className="image-asset--gif">

Copilot uses AI. Check for mistakes.
border-width: 1em 0 1em 2em;
border-style: solid;

border-color: transparent transparent transparent #202020;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The play/pause button uses a hardcoded color (#202020). This may not respect user theme preferences (light/dark mode) or brand color schemes. Consider using a CSS variable or theme token to ensure consistency across the application and proper contrast in different themes.

Suggested change
border-color: transparent transparent transparent #202020;
border-color: transparent transparent transparent var(--gif-play-button-color, #202020);

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 115
const pauseFrameUrl = await (async () => {
if (blob.type == 'image/gif' || fileType == 'image/gif') {
const gifArrayBuffer = await blob.arrayBuffer();
const gifBuffer = Buffer.from(gifArrayBuffer);
const frame0 = await GifUtil.read(gifBuffer).then(gifFile => gifFile.frames[0]);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
canvas.width = frame0.bitmap.width;
canvas.height = frame0.bitmap.height;

const imageData = ctx.createImageData(frame0.bitmap.width, frame0.bitmap.height);
imageData.data.set(frame0.bitmap.data);
ctx.putImageData(imageData, 0, 0);
}

const pauseImageBlob: Blob | null = await new Promise(resolve => {
canvas.toBlob(
blob => {
resolve(blob);
},
'image/jpeg',
0.9,
);
});

if (pauseImageBlob) {
return URL.createObjectURL(pauseImageBlob);
}
return undefined;
}
return undefined;
})();
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new GIF pause/play feature lacks test coverage. Given that the repository has comprehensive unit tests and this introduces significant new functionality with GIF processing logic and UI controls, tests should be added to verify the pause/play behavior, frame extraction, and error handling.

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 115
const pauseFrameUrl = await (async () => {
if (blob.type == 'image/gif' || fileType == 'image/gif') {
const gifArrayBuffer = await blob.arrayBuffer();
const gifBuffer = Buffer.from(gifArrayBuffer);
const frame0 = await GifUtil.read(gifBuffer).then(gifFile => gifFile.frames[0]);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
canvas.width = frame0.bitmap.width;
canvas.height = frame0.bitmap.height;

const imageData = ctx.createImageData(frame0.bitmap.width, frame0.bitmap.height);
imageData.data.set(frame0.bitmap.data);
ctx.putImageData(imageData, 0, 0);
}

const pauseImageBlob: Blob | null = await new Promise(resolve => {
canvas.toBlob(
blob => {
resolve(blob);
},
'image/jpeg',
0.9,
);
});

if (pauseImageBlob) {
return URL.createObjectURL(pauseImageBlob);
}
return undefined;
}
return undefined;
})();
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: The entire GIF file is read and the first frame is extracted for every GIF image, which can be expensive for large files. Consider adding a file size check before attempting frame extraction, or implementing caching to avoid re-processing the same GIF multiple times.

Copilot uses AI. Check for mistakes.
const url = URL.createObjectURL(blob);

const pauseFrameUrl = await (async () => {
if (blob.type == 'image/gif' || fileType == 'image/gif') {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison uses loose equality (==) instead of strict equality (===). Use strict equality to avoid type coercion issues.

Suggested change
if (blob.type == 'image/gif' || fileType == 'image/gif') {
if (blob.type === 'image/gif' || fileType === 'image/gif') {

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Dec 18, 2025

🔗 Download Full Report Artifact

🧪 Playwright Test Summary

  • Passed: 97
  • Failed: 16
  • Skipped: 6
  • 🔁 Flaky: 2
  • 📊 Total: 121
  • Total Runtime: 583.6s (~ 9 min 44 sec)
specs/Accessibility/Accessibility.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Accessibility > I should not lose a drafted message when switching between conversations in collapsed view (tags: TC-51, regression)
specs/AccountSettingsSpecs/accountSettings.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ account settings > I should not be able to change email of user managed by SCIM (tags: TC-60, regression)
specs/AppLock/AppLock.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ AppLock > Web: App should not lock if I switch back to webapp tab in time (during inactivity timeout) (tags: TC-2752, TC-2753, regression)
specs/Authentication/authentication.spec.ts (❌ 2 failed, ⚠️ 0 flaky)
  • ❌ Authentication > Verify current browser is set as temporary device (tags: TC-3460, regression)
  • ❌ Authentication > Make sure user does not see data of user of previous sessions on same browser (tags: TC-1311, regression)
specs/Block/block.spec.ts (❌ 0 failed, ⚠️ 1 flaky)
  • ⚠️ Block: User A and User B are NOT in the same team > Verify you still receive messages from blocked person in a group chat (tags: TC-141, regression)
specs/Calling/calling.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Calling > Verify 1on1 call ringing terminates on second client when accepting call (tags: TC-2823, regression)
specs/CriticalFlow/accountManagement-TC-8639.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Account Management (tags: TC-8639, crit-flow-web)
specs/CriticalFlow/Cells/uploadingFileInGroupConversation.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Uploading an file in a group conversation (tags: crit-flow-cells, regression)
specs/CriticalFlow/channelsCall-TC-8755.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Calls in channels with device switch and screenshare (tags: TC-8754, crit-flow-web)
specs/CriticalFlow/channelsManagement-TC-8752.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Channels Management (tags: TC-8752, crit-flow-web)
specs/CriticalFlow/groupCalls-TC-8632.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Planning group call with sending various messages during call (tags: TC-8632, crit-flow-web)
specs/CriticalFlow/groupVideoCall-TC-8637.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Group Video call (tags: TC-8637, crit-flow-web)
specs/CriticalFlow/joinTeam-TC-8635.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ New person joins team and setups up device (tags: TC-8635, crit-flow-web)
specs/CriticalFlow/messagesIn1On1-TC-8750.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Messages in 1:1 (tags: TC-8750, crit-flow-web)
specs/LoginSpecs/login.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Verify you can sign in by email (tags: TC-3461, regression)
specs/RegressionSpecs/block-messages.spec.ts (❌ 1 failed, ⚠️ 0 flaky)
  • ❌ Block specs (tags: TC-141, regression)
specs/Reply/reply.spec.ts (❌ 0 failed, ⚠️ 1 flaky)
  • ⚠️ Reply > I should not be able to send a reply after I got removed from the conversation (tags: TC-3014, regression)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.

Comment on lines 137 to 155
onClick={event => {
if (!isLoading) {
onClick?.(event);
}
}}
/>
<details open className="image-asset--animated">
<summary role="button" aria-label="Toggle animation playback"></summary>
<img
css={{...getImageStyle(imageSizes), ...imageStyles}}
src={assetUrl}
role="presentation"
alt={alt}
data-uie-name={isLoading ? 'image-loader' : 'image-asset-img'}
onClick={event => {
if (!isLoading) {
onClick?.(event);
}
}}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onClick handlers on both the pause frame image and the animated image can be triggered simultaneously, potentially causing duplicate modal openings or other unintended behavior. The onClick on line 137-141 will bubble up and could trigger the parent's onClick handler. Consider using event.stopPropagation() or restructure the event handlers.

Copilot uses AI. Check for mistakes.
resolve(blob);
},
'image/jpeg',
0.9,
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a magic number 0.9 as the JPEG quality parameter without explanation. Consider extracting this to a named constant like 'GIF_PAUSE_FRAME_QUALITY' with a comment explaining why 0.9 was chosen, to improve code maintainability.

Copilot uses AI. Check for mistakes.
];
const url = await getAssetUrl(image, allowedImageTypes);
const url = await getAssetUrl(image, allowedImageTypes, fileType);
if (isUnmouted.current) {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name contains a typo - 'isUnmouted' should be 'isUnmounted'. While this is a pre-existing typo, it reduces code readability and should be corrected.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 77
summary {
position: absolute;
z-index: 2;
top: 0.5rem;
right: 0.5rem;
width: 0;
height: 2em;
border-width: 1em 0 1em 2em;
border-style: solid;

border-color: transparent transparent transparent #202020;
background: transparent;
cursor: pointer;
transition: 100ms all ease;
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The play/pause button is positioned absolutely but lacks responsive positioning. On very small screens or images, the button may overflow outside the image boundaries or overlap important content. Consider adding media queries or making the button size and position responsive to the image dimensions.

Copilot uses AI. Check for mistakes.

import {useEffect, useState} from 'react';

import {GifUtil} from 'gifwrap';
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'gifwrap' library is imported but is not listed as a dependency in package.json. This will cause runtime errors when the code tries to process GIF files. Add 'gifwrap' to the dependencies section of package.json.

Copilot uses AI. Check for mistakes.
cursor: pointer;
transition: 100ms all ease;
}

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The play/pause button lacks visible focus styling. Users navigating by keyboard won't have a visual indication when the button is focused, making it difficult to know where they are on the page. Add focus styles to the summary element.

Suggested change
summary:focus,
summary:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}

Copilot uses AI. Check for mistakes.
}
}

.image-asset--animated-wrapper {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name 'image-asset--animated-wrapper' doesn't follow a consistent naming pattern with existing BEM-style classes in the codebase. Consider using a more descriptive name that indicates this is specifically for GIF controls, such as 'image-asset--gif-controls-wrapper' or following the existing pattern more closely.

Suggested change
.image-asset--animated-wrapper {
.image-asset--gif-controls-wrapper {

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +97
.image-asset--animated img {
position: absolute;
top: 0;
left: 0;
display: inline-block;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image uses absolute positioning which removes it from normal document flow. Both images (pause frame and animated) are absolutely positioned at the same location, which could cause layout issues or z-index conflicts. Ensure the positioning strategy doesn't break the layout in edge cases.

Suggested change
.image-asset--animated img {
position: absolute;
top: 0;
left: 0;
display: inline-block;
.image-asset--animated {
position: relative;
display: inline-block;
}
.image-asset--animated img {
display: block;
width: 100%;
height: auto;

Copilot uses AI. Check for mistakes.
public readonly downloadProgress: ko.PureComputed<number | undefined>;
public readonly cancelDownload: () => void;
public file_size: number;
public readonly file_type: string;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field is declared as 'readonly' but initialized to an empty string in the constructor, which contradicts the readonly semantics. Either remove 'readonly' to allow modification, or ensure file_type is set at construction time via a constructor parameter.

Suggested change
public readonly file_type: string;
public file_type: string;

Copilot uses AI. Check for mistakes.
Comment on lines 81 to 82
const gifBuffer = Buffer.from(gifArrayBuffer);
const frame0 = await GifUtil.read(gifBuffer).then(gifFile => gifFile.frames[0]);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buffer is a Node.js global that is not available in browser environments by default. This will cause a runtime error unless a polyfill is configured. Use Uint8Array instead, which is native to browsers, or ensure the Buffer polyfill is properly set up.

Suggested change
const gifBuffer = Buffer.from(gifArrayBuffer);
const frame0 = await GifUtil.read(gifBuffer).then(gifFile => gifFile.frames[0]);
const gifBytes = new Uint8Array(gifArrayBuffer);
const frame0 = await GifUtil.read(gifBytes).then(gifFile => gifFile.frames[0]);

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

opacity: 1;
}

[open] summary {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS selector [open] on line 79 is missing the element type qualifier. It should be details[open] to be more specific and match the pattern used elsewhere in the file (e.g., details summary on lines 85 and 89). This improves specificity and makes the selector's intent clearer.

Suggested change
[open] summary {
details[open] summary {

Copilot uses AI. Check for mistakes.
border-color: transparent transparent transparent #202020;
background: transparent;
cursor: pointer;
opacity: 0.1;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opacity value of 0.1 makes the play/pause button nearly invisible by default. This could make it difficult for users to discover this functionality. Consider using a higher opacity value (e.g., 0.5-0.7) or adding a subtle background to ensure the button is more discoverable while still being unobtrusive.

Suggested change
opacity: 0.1;
opacity: 0.6;

Copilot uses AI. Check for mistakes.
const imageData = ctx.createImageData(frame0.bitmap.width, frame0.bitmap.height);
imageData.data.set(frame0.bitmap.data);
ctx.putImageData(imageData, 0, 0);

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "umounted" should be "unmounted"

Copilot uses AI. Check for mistakes.
{imageUrl?.pauseFrameUrl ? (
<div className="image-asset--animated-wrapper">
<ImageElement src={imageUrl?.pauseFrameUrl} />
<details open className="image-asset--animated">
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details element is set to open by default, which means GIFs will always start playing automatically. This contradicts the pause/play feature's purpose and may be jarring for users as noted in the PR description. Consider removing the open attribute so GIFs start paused, or make this configurable based on user preference.

Suggested change
<details open className="image-asset--animated">
<details className="image-asset--animated">

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +68
getAssetUrl: async (
resource: AssetRemoteData,
acceptedMimeTypes?: string[],
fileType?: string,
): Promise<AssetUrl> => {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fileType parameter is optional but there's no documentation explaining when it should be provided or how it relates to blob.type. The condition checks both blob.type === 'image/gif' OR fileType === 'image/gif', which suggests they might differ in some cases. Add a JSDoc comment explaining when and why fileType should be passed as a parameter.

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +149
<details open className="image-asset--animated">
<summary role="button" aria-label="Toggle animation playback"></summary>
<ImageElement src={assetUrl} />
</details>
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the HTML details/summary elements for a play/pause toggle is semantically incorrect. These elements are designed for expandable/collapsible content disclosure, not for media controls. Consider using a standard button element with proper ARIA attributes like aria-pressed to indicate the play/pause state, which would better convey the control's purpose to assistive technologies.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants