From 22a3aa0ed79072869603e3899e56303db80b6000 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:28:59 -0700 Subject: [PATCH] Introduce FileThumbnail component --- images/caption-shadow.svg | 2 +- images/chat-session-refresh.svg | 2 +- images/check-circle-outline.svg | 2 +- images/delivery-issue.svg | 14 +-- images/double-check.svg | 2 +- images/file.svg | 2 +- images/generic-file-dangerous.svg | 1 + images/generic-file.svg | 2 +- images/gift-bow.svg | 17 +--- images/gift-thumbnail.svg | 20 +---- images/image.svg | 2 +- images/local-delete-sync.svg | 2 +- images/macos-switch.svg | 18 +--- images/merged-chat.svg | 13 +-- images/mobile-settings-dark.svg | 2 +- images/mobile-settings-light.svg | 2 +- images/movie.svg | 2 +- images/phone_40_color.svg | 8 +- images/phone_40_color_dark.svg | 18 +--- images/profile-avatar.svg | 20 +---- images/profile-moved-dark.svg | 2 +- images/profile-moved.svg | 2 +- images/qr_codes_40_color.svg | 33 +------ images/qr_codes_40_color_dark.svg | 31 +------ images/read.svg | 2 +- images/rocket-160.svg | 42 +-------- images/rocket-36-dark.svg | 32 +------ images/rocket-36-light.svg | 36 +------- images/safety-number-migration.svg | 43 +--------- images/sending.svg | 2 +- images/signal-logo-and-wordmark.svg | 27 +----- images/signal-logo.svg | 2 +- images/spinner-24.svg | 2 +- images/spinner-56.svg | 2 +- images/spinner-track-24.svg | 2 +- images/spinner-track-56.svg | 2 +- images/titlebar_icon.svg | 2 +- images/usernames_40_color.svg | 6 +- images/usernames_40_color_dark.svg | 5 +- images/x-shadow-16.svg | 2 +- package.json | 2 +- stylesheets/_modules.scss | 86 +++---------------- ts/components/FileThumbnail.stories.tsx | 42 +++++++++ ts/components/FileThumbnail.tsx | 49 +++++++++++ ts/components/conversation/Message.tsx | 35 +------- .../conversation/StagedGenericAttachment.tsx | 14 +-- 46 files changed, 150 insertions(+), 506 deletions(-) create mode 100644 images/generic-file-dangerous.svg create mode 100644 ts/components/FileThumbnail.stories.tsx create mode 100644 ts/components/FileThumbnail.tsx diff --git a/images/caption-shadow.svg b/images/caption-shadow.svg index 42ab5a93a2..444ae5b9a7 100644 --- a/images/caption-shadow.svg +++ b/images/caption-shadow.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/chat-session-refresh.svg b/images/chat-session-refresh.svg index 812fce788f..64c39bd701 100644 --- a/images/chat-session-refresh.svg +++ b/images/chat-session-refresh.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/check-circle-outline.svg b/images/check-circle-outline.svg index 2c03039440..c256ebbb8c 100644 --- a/images/check-circle-outline.svg +++ b/images/check-circle-outline.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/delivery-issue.svg b/images/delivery-issue.svg index 64edbf0904..2f3b730a33 100644 --- a/images/delivery-issue.svg +++ b/images/delivery-issue.svg @@ -1,13 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/double-check.svg b/images/double-check.svg index 79b9fcdbd1..fec1f9dae5 100644 --- a/images/double-check.svg +++ b/images/double-check.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/file.svg b/images/file.svg index dedeef231a..5169740223 100644 --- a/images/file.svg +++ b/images/file.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/generic-file-dangerous.svg b/images/generic-file-dangerous.svg new file mode 100644 index 0000000000..026df2e326 --- /dev/null +++ b/images/generic-file-dangerous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/generic-file.svg b/images/generic-file.svg index e01844db9c..0d4e35abc3 100644 --- a/images/generic-file.svg +++ b/images/generic-file.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/gift-bow.svg b/images/gift-bow.svg index 2798d1bfc4..1f4eb1d639 100644 --- a/images/gift-bow.svg +++ b/images/gift-bow.svg @@ -1,16 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/images/gift-thumbnail.svg b/images/gift-thumbnail.svg index 2199099269..00fa772fe4 100644 --- a/images/gift-thumbnail.svg +++ b/images/gift-thumbnail.svg @@ -1,19 +1 @@ - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/images/image.svg b/images/image.svg index 21ba4507b9..c274341862 100644 --- a/images/image.svg +++ b/images/image.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/local-delete-sync.svg b/images/local-delete-sync.svg index dee360ed29..53e332d932 100644 --- a/images/local-delete-sync.svg +++ b/images/local-delete-sync.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/macos-switch.svg b/images/macos-switch.svg index 6393b5ff60..ec589b2d17 100644 --- a/images/macos-switch.svg +++ b/images/macos-switch.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/merged-chat.svg b/images/merged-chat.svg index 06dab79558..26f5a5bb7f 100644 --- a/images/merged-chat.svg +++ b/images/merged-chat.svg @@ -1,12 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/mobile-settings-dark.svg b/images/mobile-settings-dark.svg index 20c58c0418..3eb1baedc7 100644 --- a/images/mobile-settings-dark.svg +++ b/images/mobile-settings-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/mobile-settings-light.svg b/images/mobile-settings-light.svg index 76ff934d06..a2555ecf96 100644 --- a/images/mobile-settings-light.svg +++ b/images/mobile-settings-light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/movie.svg b/images/movie.svg index 0cec0e7bb5..8e9b729747 100644 --- a/images/movie.svg +++ b/images/movie.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/phone_40_color.svg b/images/phone_40_color.svg index 9e2700e2ac..cbd1415eff 100644 --- a/images/phone_40_color.svg +++ b/images/phone_40_color.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/images/phone_40_color_dark.svg b/images/phone_40_color_dark.svg index 30f3a3f6bc..c62e5430d6 100644 --- a/images/phone_40_color_dark.svg +++ b/images/phone_40_color_dark.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/profile-avatar.svg b/images/profile-avatar.svg index 50772c4fe0..01654d4883 100644 --- a/images/profile-avatar.svg +++ b/images/profile-avatar.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/profile-moved-dark.svg b/images/profile-moved-dark.svg index bbe3474438..4adc6a85c1 100644 --- a/images/profile-moved-dark.svg +++ b/images/profile-moved-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/profile-moved.svg b/images/profile-moved.svg index 3c1feede3e..39287d69fc 100644 --- a/images/profile-moved.svg +++ b/images/profile-moved.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/qr_codes_40_color.svg b/images/qr_codes_40_color.svg index a660b384b5..531f699ddc 100644 --- a/images/qr_codes_40_color.svg +++ b/images/qr_codes_40_color.svg @@ -1,32 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/qr_codes_40_color_dark.svg b/images/qr_codes_40_color_dark.svg index 1cad44a0b5..4fd3d08cc8 100644 --- a/images/qr_codes_40_color_dark.svg +++ b/images/qr_codes_40_color_dark.svg @@ -1,30 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/read.svg b/images/read.svg index 1083ee9cd8..152a772242 100644 --- a/images/read.svg +++ b/images/read.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/rocket-160.svg b/images/rocket-160.svg index 4dc3f96671..275781af2d 100644 --- a/images/rocket-160.svg +++ b/images/rocket-160.svg @@ -1,41 +1 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/rocket-36-dark.svg b/images/rocket-36-dark.svg index 5ec0c73975..c5f1325bd4 100644 --- a/images/rocket-36-dark.svg +++ b/images/rocket-36-dark.svg @@ -1,31 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/rocket-36-light.svg b/images/rocket-36-light.svg index b946f9513e..ab13c60bb7 100644 --- a/images/rocket-36-light.svg +++ b/images/rocket-36-light.svg @@ -1,35 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/safety-number-migration.svg b/images/safety-number-migration.svg index 75d875b29a..f6299bb22e 100644 --- a/images/safety-number-migration.svg +++ b/images/safety-number-migration.svg @@ -1,42 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/sending.svg b/images/sending.svg index 5f4c58e59c..05e776fb25 100644 --- a/images/sending.svg +++ b/images/sending.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/signal-logo-and-wordmark.svg b/images/signal-logo-and-wordmark.svg index e90dedda61..46ed3ad81b 100644 --- a/images/signal-logo-and-wordmark.svg +++ b/images/signal-logo-and-wordmark.svg @@ -1,26 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/signal-logo.svg b/images/signal-logo.svg index 036d0696f6..61f00c2066 100644 --- a/images/signal-logo.svg +++ b/images/signal-logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/spinner-24.svg b/images/spinner-24.svg index 3544360267..fca8aa872e 100644 --- a/images/spinner-24.svg +++ b/images/spinner-24.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/spinner-56.svg b/images/spinner-56.svg index d5a0e60ca4..3fec7503fd 100644 --- a/images/spinner-56.svg +++ b/images/spinner-56.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/spinner-track-24.svg b/images/spinner-track-24.svg index a209386503..b2ba900da9 100644 --- a/images/spinner-track-24.svg +++ b/images/spinner-track-24.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/spinner-track-56.svg b/images/spinner-track-56.svg index 631863d731..e31eea4a96 100644 --- a/images/spinner-track-56.svg +++ b/images/spinner-track-56.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/titlebar_icon.svg b/images/titlebar_icon.svg index 012b952653..eb5c1793ec 100644 --- a/images/titlebar_icon.svg +++ b/images/titlebar_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/usernames_40_color.svg b/images/usernames_40_color.svg index 710e7915d2..b69d7b0a27 100644 --- a/images/usernames_40_color.svg +++ b/images/usernames_40_color.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/images/usernames_40_color_dark.svg b/images/usernames_40_color_dark.svg index 842d05b0cc..999459a73a 100644 --- a/images/usernames_40_color_dark.svg +++ b/images/usernames_40_color_dark.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/images/x-shadow-16.svg b/images/x-shadow-16.svg index 88a5250760..f778587cb6 100644 --- a/images/x-shadow-16.svg +++ b/images/x-shadow-16.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/package.json b/package.json index c2a5b43e97..ece1a5359b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "danger:local": "./danger/danger.sh local --base main", "danger:ci": "./danger/danger.sh ci --base origin/main", "format": "pprettier --write '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'", - "svgo": "svgo --multipass images/**/*.svg", + "svgo": "svgo --multipass images/**/*.svg images/*.svg", "transpile": "run-p check:types build:esbuild", "check:types": "tsc --noEmit", "clean-transpile": "node ./scripts/clean-transpile.js", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 35121977b8..5f2f9bbeba 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -711,42 +711,6 @@ $message-padding-horizontal: 12px; padding-top: 4px; } -.module-message__simple-attachment__icon { - background: url('../images/generic-file.svg') no-repeat center; - height: 40px; - width: 30px; - margin-inline-start: 3px; - margin-inline-end: 3px; - - // So we can center the extension text inside this icon - display: flex; - flex-direction: row; - align-items: center; -} - -.module-message__simple-attachment__icon-dangerous-container { - position: absolute; - - top: -1px; - inset-inline-end: -4px; - - height: 16px; - width: 16px; - - border-radius: 50%; - background-color: variables.$color-white; -} - -.module-message__simple-attachment__icon-dangerous { - height: 16px; - width: 16px; - - @include mixins.color-svg( - '../images/icons/v2/error-solid-24.svg', - variables.$color-accent-red - ); -} - .module-message__simple-attachment__icon__extension { @include mixins.font-subtitle; text-transform: lowercase; @@ -3345,11 +3309,17 @@ button.module-image__border-overlay:focus { // Module: Staged Generic Attachment .module-staged-generic-attachment { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + height: 120px; width: 120px; - display: inline-block; position: relative; border-radius: 4px; + padding: 7px; vertical-align: middle; white-space: nowrap; @@ -3365,6 +3335,10 @@ button.module-image__border-overlay:focus { } .module-staged-generic-attachment__close-button { + position: absolute; + inset-inline-end: 5px; + top: 0; + @include mixins.staged-attachment-close-button; & { @@ -3383,49 +3357,13 @@ button.module-image__border-overlay:focus { } } -.module-staged-generic-attachment__icon { - margin-top: 30px; - - background: url('../images/generic-file.svg') no-repeat center; - height: 44px; - width: 56px; - margin-inline: 32px; - margin-bottom: -4px; - - // So we can center the extension text inside this icon - display: flex; - flex-direction: row; - align-items: center; -} - -.module-staged-generic-attachment__icon__extension { - font-size: 10px; - line-height: 13px; - letter-spacing: 0.1px; - text-transform: uppercase; - - // Along with flow layout in parent item, centers text - text-align: center; - width: 25px; - margin-inline: auto; - - // We don't have much room for text here, cut it off without ellipse - overflow-x: hidden; - white-space: nowrap; - text-overflow: clip; - - color: variables.$color-gray-90; -} - .module-staged-generic-attachment__filename { @include mixins.font-caption; - margin: 7px; - margin-top: 5px; + max-width: 100%; text-align: center; overflow: hidden; - height: 2.4em; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; diff --git a/ts/components/FileThumbnail.stories.tsx b/ts/components/FileThumbnail.stories.tsx new file mode 100644 index 0000000000..462eba621b --- /dev/null +++ b/ts/components/FileThumbnail.stories.tsx @@ -0,0 +1,42 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import type { Meta } from '@storybook/react'; +import { FileThumbnail } from './FileThumbnail'; +import { APPLICATION_OCTET_STREAM } from '../types/MIME'; + +export default { + title: 'FileThumbnail', +} satisfies Meta; + +export function ThreeLetterExtension(): JSX.Element { + return ( + + ); +} + +export function FourLetterExtension(): JSX.Element { + return ( + + ); +} + +export function ManyLetterExtension(): JSX.Element { + return ( + + ); +} + +export function OnlyContentType(): JSX.Element { + return ; +} + +export function DangerousExtension(): JSX.Element { + return ( + + ); +} diff --git a/ts/components/FileThumbnail.tsx b/ts/components/FileThumbnail.tsx new file mode 100644 index 0000000000..b2fec1f097 --- /dev/null +++ b/ts/components/FileThumbnail.tsx @@ -0,0 +1,49 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; + +import { getExtensionForDisplay } from '../types/Attachment'; +import { isFileDangerous } from '../util/isFileDangerous'; +import { tw } from '../axo/tw'; + +export type PropsType = Readonly[0]>; + +export function FileThumbnail(props: PropsType): JSX.Element { + const extension = getExtensionForDisplay(props) ?? ''; + const isDangerous = isFileDangerous(props.fileName || ''); + + return ( + + 3 ? 'text-[9px]' : '', + 'type-caption whitespace-nowrap' + )} + > + {extension} + + {isDangerous ? ( + + ) : null} + + ); +} diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 5eb9ac9ed4..0ab52af192 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -58,7 +58,6 @@ import type { } from '../../types/Attachment'; import { canDisplayImage, - getExtensionForDisplay, getGridDimensions, getImageDimensionsForTimeline, hasImage, @@ -107,9 +106,9 @@ import { formatFileSize } from '../../util/formatFileSize'; import { AttachmentNotAvailableModalType } from '../AttachmentNotAvailableModal'; import { assertDev, strictAssert } from '../../util/assert'; import { AttachmentStatusIcon } from './AttachmentStatusIcon'; -import { isFileDangerous } from '../../util/isFileDangerous'; import { TapToViewNotAvailableType } from '../TapToViewNotAvailableModal'; import type { DataPropsType as TapToViewNotAvailablePropsType } from '../TapToViewNotAvailableModal'; +import { FileThumbnail } from '../FileThumbnail'; import { FunStaticEmoji } from '../fun/FunEmoji'; import { type EmojifyData, @@ -1372,37 +1371,7 @@ export class Message extends React.PureComponent { const isIncoming = direction === 'incoming'; const renderAttachmentDownloaded = () => { - const extension = getExtensionForDisplay({ contentType, fileName }); - const isDangerous = isFileDangerous(fileName || ''); - const moreChar = extension && extension.length > 3; - const extensionForDisplay = - extension && extension.length > 4 - ? `${extension.slice(0, 3)}…` - : extension; - - return ( - <> - - {extension ? ( - - {extensionForDisplay} - - ) : null} - - {isDangerous ? ( - - - - ) : null} - > - ); + return ; }; const willShowMetadata = diff --git a/ts/components/conversation/StagedGenericAttachment.tsx b/ts/components/conversation/StagedGenericAttachment.tsx index 300729c4fa..35555322bf 100644 --- a/ts/components/conversation/StagedGenericAttachment.tsx +++ b/ts/components/conversation/StagedGenericAttachment.tsx @@ -4,8 +4,8 @@ import React from 'react'; import type { AttachmentType } from '../../types/Attachment'; -import { getExtensionForDisplay } from '../../types/Attachment'; import type { LocalizerType } from '../../types/Util'; +import { FileThumbnail } from '../FileThumbnail'; export type Props = { attachment: AttachmentType; @@ -18,8 +18,7 @@ export function StagedGenericAttachment({ i18n, onClose, }: Props): JSX.Element { - const { fileName, contentType } = attachment; - const extension = getExtensionForDisplay({ contentType, fileName }); + const { fileName } = attachment; return ( @@ -33,13 +32,8 @@ export function StagedGenericAttachment({ } }} /> - - {extension ? ( - - {extension} - - ) : null} - + + {fileName}