Show info for permanently undownloadable visual attachments

This commit is contained in:
ayumi-signal 2025-01-15 09:15:32 -08:00 committed by GitHub
parent 6451ff0cf1
commit 1cc26d5cc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 314 additions and 15 deletions

View file

@ -1526,6 +1526,14 @@
"messageformat": "{count, plural, one {# item} other {# items}}",
"description": "Describes a button shown on a grid of attachments to start of them downloading"
},
"icu:mediaNoLongerAvailable": {
"messageformat": "This media is no longer available.",
"description": "Shown in info toast for messages with old image and video attachments which are no longer available for download. Also used for accessibility label for the download attachment button."
},
"icu:attachmentNoLongerAvailable__learnMore": {
"messageformat": "Learn more",
"description": "Link in message placeholder and info toast for messages with old attachments which are no longer available for download."
},
"icu:save": {
"messageformat": "Save",
"description": "Used on save buttons"

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.58705 1.55585C2.30229 1.2711 1.84061 1.2711 1.55585 1.55585C1.2711 1.84061 1.2711 2.30229 1.55585 2.58705L17.3892 18.4204C17.6739 18.7051 18.1356 18.7051 18.4204 18.4204C18.7051 18.1356 18.7051 17.6739 18.4204 17.3892L2.58705 1.55585Z" fill="#000"/>
<path d="M1.75197 4.59525C1.75695 4.58548 1.76197 4.57573 1.76704 4.566L2.90669 5.70565C2.88314 5.82821 2.86432 5.97187 2.85024 6.14414C2.8135 6.59392 2.81293 7.17165 2.81293 8.00043L2.81293 12.0004C2.81293 12.4902 2.81313 12.8923 2.82087 13.2321L4.97298 11.08C5.78224 10.2708 7.02639 10.1614 7.95312 10.7521L9.94988 12.7488L9.47436 13.2244C9.1896 13.5091 8.72792 13.5091 8.44316 13.2244L7.33001 12.1112C6.96389 11.7451 6.3703 11.7451 6.00418 12.1112L3.16888 14.9465C3.37706 15.2653 3.66568 15.5254 4.00732 15.6995C4.19935 15.7973 4.45288 15.8646 4.89414 15.9006C5.34392 15.9374 5.92165 15.9379 6.75043 15.9379H13.139L14.5871 17.3861C14.2061 17.3963 13.7735 17.3963 13.2817 17.3963H6.7192C5.92894 17.3963 5.29155 17.3963 4.77539 17.3541C4.24394 17.3107 3.77713 17.2189 3.34525 16.9989C2.65925 16.6494 2.1015 16.0916 1.75197 15.4056C1.53191 14.9737 1.44017 14.5069 1.39675 13.9755C1.35458 13.4593 1.35459 12.8219 1.35459 12.0317V7.9692C1.35459 7.17894 1.35458 6.54155 1.39675 6.02539C1.44017 5.49394 1.53191 5.02713 1.75197 4.59525Z" fill="#000"/>
<path d="M15.0279 8.99667C14.1184 8.08718 12.6596 8.06177 11.7194 8.92044L12.7534 9.95442C13.1213 9.66366 13.6569 9.68814 13.9967 10.0279L17.1804 13.2116C17.1751 13.4554 17.1661 13.6674 17.1506 13.8567C17.1365 14.029 17.1177 14.1726 17.0942 14.2952L18.2338 15.4349C18.2389 15.4251 18.2439 15.4154 18.2489 15.4056C18.4689 14.9737 18.5607 14.5069 18.6041 13.9755C18.6463 13.4593 18.6463 12.8219 18.6463 12.0317V7.96922C18.6463 7.17895 18.6463 6.54155 18.6041 6.02539C18.5607 5.49394 18.4689 5.02713 18.2489 4.59525C17.8994 3.90925 17.3416 3.35151 16.6556 3.00197C16.2237 2.78191 15.7569 2.69018 15.2255 2.64675C14.7093 2.60458 14.0719 2.60459 13.2817 2.60459H6.71921C6.22739 2.60459 5.79475 2.60459 5.41371 2.61475L6.86189 4.06293L13.2504 4.06293C14.0792 4.06293 14.6569 4.0635 15.1067 4.10024C15.548 4.1363 15.8015 4.20351 15.9935 4.30135C16.4051 4.51107 16.7398 4.84572 16.9495 5.25732C17.0473 5.44935 17.1146 5.70288 17.1506 6.14415C17.1874 6.59392 17.1879 7.17165 17.1879 8.00043V11.1567L15.0279 8.99667Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -2864,6 +2864,7 @@ button.module-image__border-overlay:focus {
bottom: 0;
z-index: variables.$z-index-base;
inset-inline: 0;
pointer-events: none;
}
.module-image__overlay-circle {
@ -2877,6 +2878,10 @@ button.module-image__border-overlay:focus {
}
}
.module-image__overlay-circle--undownloadable {
background-color: variables.$color-black-alpha-40;
}
.module-image__play-icon {
@include mixins.position-absolute-center;
@ -2908,6 +2913,16 @@ button.module-image__border-overlay:focus {
variables.$color-white
);
}
.module-image__undownloadable-icon {
@include mixins.position-absolute-center;
height: 24px;
width: 24px;
@include mixins.color-svg(
'../images/icons/v3/photo/photo-slash-compact.svg',
variables.$color-white
);
}
.module-image__text-container {
position: absolute;

View file

@ -65,6 +65,7 @@ const MESSAGE_DEFAULT_PROPS = {
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMediaNoLongerAvailableToast: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled,
textDirection: TextDirection.Default,
viewStory: shouldNeverBeCalled,

View file

@ -73,6 +73,7 @@ const MESSAGE_DEFAULT_PROPS = {
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMediaNoLongerAvailableToast: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled,
theme: ThemeType.dark,
viewStory: shouldNeverBeCalled,

View file

@ -129,6 +129,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.LoadingFullLogs };
case ToastType.MaxAttachments:
return { toastType: ToastType.MaxAttachments };
case ToastType.MediaNoLongerAvailable:
return { toastType: ToastType.MediaNoLongerAvailable };
case ToastType.MessageBodyTooLong:
return { toastType: ToastType.MessageBodyTooLong };
case ToastType.MessageLoop:

View file

@ -16,6 +16,8 @@ import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
import type { AnyActionableMegaphone } from '../types/Megaphone';
import { MegaphoneType } from '../types/Megaphone';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { LINKED_DEVICES_URL } from '../types/support';
export type PropsType = {
hideToast: () => unknown;
@ -407,6 +409,20 @@ export function renderToast({
return <Toast onClose={hideToast}>{i18n('icu:maximumAttachments')}</Toast>;
}
if (toastType === ToastType.MediaNoLongerAvailable) {
return (
<Toast
onClose={hideToast}
toastAction={{
label: i18n('icu:attachmentNoLongerAvailable__learnMore'),
onClick: () => openLinkInWebBrowser(LINKED_DEVICES_URL),
}}
>
{i18n('icu:mediaNoLongerAvailable')}
</Toast>
);
}
if (toastType === ToastType.MessageBodyTooLong) {
return <Toast onClose={hideToast}>{i18n('icu:messageBodyTooLong')}</Toast>;
}

View file

@ -12,12 +12,14 @@ import {
hasNotResolved,
getImageDimensions,
defaultBlurHash,
isDownloadable,
} from '../../types/Attachment';
import * as Errors from '../../types/errors';
import * as log from '../../logging/log';
import { useReducedMotion } from '../../hooks/useReducedMotion';
import { AttachmentDetailPill } from './AttachmentDetailPill';
import { getSpinner } from './Image';
import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler';
const MAX_GIF_REPEAT = 4;
const MAX_GIF_TIME = 8;
@ -33,6 +35,7 @@ export type Props = {
readonly theme?: ThemeType;
onError(): void;
showMediaNoLongerAvailableToast?: () => void;
showVisualAttachment(): void;
startDownload(): void;
cancelDownload(): void;
@ -51,6 +54,7 @@ export function GIF(props: Props): JSX.Element {
theme,
onError,
showMediaNoLongerAvailableToast,
showVisualAttachment,
startDownload,
cancelDownload,
@ -123,6 +127,10 @@ export function GIF(props: Props): JSX.Element {
setIsPlaying(isFocused && !isTapToPlayPaused);
}, [isFocused, playTime, currentTime, repeatCount, tapToPlay]);
const undownloadableClick = useUndownloadableMediaHandler(
showMediaNoLongerAvailableToast
);
const onTimeUpdate = async (event: MediaEvent): Promise<void> => {
const { currentTime: reportedTime } = event.currentTarget;
if (!Number.isNaN(reportedTime)) {
@ -175,9 +183,10 @@ export function GIF(props: Props): JSX.Element {
const isPending = Boolean(attachment.pending);
const isNotResolved = hasNotResolved(attachment) && !isPending;
const isMediaDownloadable = isDownloadable(attachment);
let gif: JSX.Element | undefined;
if (isNotResolved || isPending) {
if (isNotResolved || isPending || !isMediaDownloadable) {
gif = (
<Blurhash
hash={attachment.blurHash || defaultBlurHash(theme)}
@ -241,7 +250,7 @@ export function GIF(props: Props): JSX.Element {
});
let overlay: JSX.Element | undefined;
if ((tapToPlay && !isPlaying) || isNotResolved) {
if ((tapToPlay && !isPlaying) || (isNotResolved && isMediaDownloadable)) {
const className = classNames([
'module-image__border-overlay',
'module-image__border-overlay--with-click-handler',
@ -262,9 +271,21 @@ export function GIF(props: Props): JSX.Element {
<span />
</button>
);
} else if (!isMediaDownloadable) {
overlay = (
<button
type="button"
className="module-image__overlay-circle module-image__overlay-circle--undownloadable"
aria-label={i18n('icu:mediaNoLongerAvailable')}
onClick={undownloadableClick}
tabIndex={tabIndex}
>
<div className="module-image__undownloadable-icon" />
</button>
);
}
const detailPill = (
const detailPill = isDownloadable(attachment) ? (
<AttachmentDetailPill
attachments={[attachment]}
cancelDownload={cancelDownload}
@ -272,7 +293,7 @@ export function GIF(props: Props): JSX.Element {
isGif
startDownload={startDownload}
/>
);
) : null;
return (
<div className="module-image module-image--gif">

View file

@ -14,10 +14,12 @@ import type {
} from '../../types/Attachment';
import {
defaultBlurHash,
isDownloadable,
isIncremental,
isReadyToView,
} from '../../types/Attachment';
import { ProgressCircle } from '../ProgressCircle';
import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler';
export enum CurveType {
None = 0,
@ -55,6 +57,7 @@ export type Props = {
i18n: LocalizerType;
theme?: ThemeType;
showMediaNoLongerAvailableToast?: () => void;
showVisualAttachment?: (attachment: AttachmentType) => void;
cancelDownload?: () => void;
startDownload?: () => void;
@ -78,6 +81,7 @@ export function Image({
i18n,
noBackground,
noBorder,
showMediaNoLongerAvailableToast,
showVisualAttachment,
startDownload,
cancelDownload,
@ -164,6 +168,9 @@ export function Image({
},
[startDownload]
);
const undownloadableClick = useUndownloadableMediaHandler(
showMediaNoLongerAvailableToast
);
const imageOrBlurHash = url ? (
<img
@ -211,6 +218,8 @@ export function Image({
tabIndex,
});
const isMediaDownloadable = isDownloadable(attachment);
return (
<div
className={classNames(
@ -226,7 +235,19 @@ export function Image({
}}
>
{imageOrBlurHash}
{startDownloadButton}
{isMediaDownloadable ? (
startDownloadButton
) : (
<button
type="button"
className="module-image__overlay-circle module-image__overlay-circle--undownloadable"
aria-label={i18n('icu:mediaNoLongerAvailable')}
onClick={undownloadableClick}
tabIndex={tabIndex}
>
<div className="module-image__undownloadable-icon" />
</button>
)}
{spinner}
{attachment.caption ? (
@ -245,7 +266,9 @@ export function Image({
}}
/>
) : null}
{(attachment.path || isIncremental(attachment)) && playIconOverlay ? (
{(attachment.path || isIncremental(attachment)) &&
isMediaDownloadable &&
playIconOverlay ? (
<div className="module-image__overlay-circle">
<div className="module-image__play-icon" />
</div>
@ -267,7 +290,9 @@ export function Image({
style={curveStyles}
/>
) : null}
{showVisualAttachment && isReadyToView(attachment) ? (
{showVisualAttachment &&
isReadyToView(attachment) &&
isMediaDownloadable ? (
<button
type="button"
className={classNames('module-image__border-overlay', {

View file

@ -14,6 +14,7 @@ import {
getImageDimensions,
getThumbnailUrl,
getUrl,
isDownloadable,
isIncremental,
isVideoAttachment,
} from '../../types/Attachment';
@ -42,6 +43,7 @@ export type Props = {
onError: () => void;
showVisualAttachment: (attachment: AttachmentType) => void;
showMediaNoLongerAvailableToast: () => void;
cancelDownload: () => void;
startDownload: () => void;
};
@ -112,6 +114,7 @@ export function ImageGrid({
isSticker,
stickerSize,
onError,
showMediaNoLongerAvailableToast,
showVisualAttachment,
cancelDownload,
startDownload,
@ -158,9 +161,13 @@ export function ImageGrid({
return null;
}
const downloadableAttachments = attachments.filter(attachment =>
isDownloadable(attachment)
);
const detailPill = (
<AttachmentDetailPill
attachments={attachments}
attachments={downloadableAttachments}
i18n={i18n}
startDownload={startDownload}
cancelDownload={cancelDownload}
@ -207,6 +214,7 @@ export function ImageGrid({
getUrl(attachments[0]) ?? attachments[0].thumbnailFromBackup?.url
}
tabIndex={tabIndex}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={startDownload}
@ -235,6 +243,7 @@ export function ImageGrid({
width={150}
cropWidth={GAP}
url={getThumbnailUrl(attachments[0])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -254,6 +263,7 @@ export function ImageGrid({
width={150}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -283,6 +293,7 @@ export function ImageGrid({
width={200}
cropWidth={GAP}
url={getUrl(attachments[0])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -301,6 +312,7 @@ export function ImageGrid({
attachment={attachments[1]}
playIconOverlay={isVideoAttachment(attachments[1])}
url={getThumbnailUrl(attachments[1])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -319,6 +331,7 @@ export function ImageGrid({
attachment={attachments[2]}
playIconOverlay={isVideoAttachment(attachments[2])}
url={getThumbnailUrl(attachments[2])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -350,6 +363,7 @@ export function ImageGrid({
cropHeight={GAP}
cropWidth={GAP}
url={getThumbnailUrl(attachments[0])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -368,6 +382,7 @@ export function ImageGrid({
cropHeight={GAP}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -389,6 +404,7 @@ export function ImageGrid({
cropWidth={GAP}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -407,6 +423,7 @@ export function ImageGrid({
width={150}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -441,6 +458,7 @@ export function ImageGrid({
width={150}
cropWidth={GAP}
url={getThumbnailUrl(attachments[0])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -457,6 +475,7 @@ export function ImageGrid({
width={150}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -478,6 +497,7 @@ export function ImageGrid({
cropWidth={GAP}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -496,6 +516,7 @@ export function ImageGrid({
cropWidth={GAP}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={cancelDownload}
startDownload={downloadPill ? undefined : startDownload}
@ -516,6 +537,7 @@ export function ImageGrid({
overlayText={moreMessagesOverlayText}
attachment={attachments[4]}
url={getThumbnailUrl(attachments[4])}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showVisualAttachment={showVisualAttachment}
cancelDownload={undefined}
startDownload={undefined}
@ -548,6 +570,13 @@ function renderDownloadPill({
return null;
}
const noneDownloadable = !attachments.some(attachment =>
isDownloadable(attachment)
);
if (noneDownloadable) {
return null;
}
return (
<button
type="button"

View file

@ -70,6 +70,7 @@ import {
isVideo,
isGIF,
isPlayed,
isDownloadable,
} from '../../types/Attachment';
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
@ -375,6 +376,7 @@ export type PropsActions = {
showAttachmentDownloadStillInProgressToast: (count: number) => unknown;
showExpiredIncomingTapToViewToast: () => unknown;
showExpiredOutgoingTapToViewToast: () => unknown;
showMediaNoLongerAvailableToast: () => unknown;
viewStory: ViewStoryActionCreatorType;
onToggleSelect: (selected: boolean, shift: boolean) => void;
@ -945,6 +947,7 @@ export class Message extends React.PureComponent<Props, State> {
shouldCollapseAbove,
shouldCollapseBelow,
showLightbox,
showMediaNoLongerAvailableToast,
status,
text,
textAttachment,
@ -1008,6 +1011,7 @@ export class Message extends React.PureComponent<Props, State> {
messageId: id,
});
}}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
/>
</div>
);
@ -1043,11 +1047,13 @@ export class Message extends React.PureComponent<Props, State> {
cancelDownload={() => {
cancelAttachmentDownload({ messageId: id });
}}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
/>
</div>
);
}
}
if (isAudio(attachments)) {
const played = isPlayed(direction, status, readStatus);
@ -1165,6 +1171,7 @@ export class Message extends React.PureComponent<Props, State> {
id,
kickOffAttachmentDownload,
cancelAttachmentDownload,
showMediaNoLongerAvailableToast,
previews,
quote,
shouldCollapseAbove,
@ -1237,6 +1244,7 @@ export class Message extends React.PureComponent<Props, State> {
cancelDownload={() => {
cancelAttachmentDownload({ messageId: id });
}}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
/>
) : null}
<div dir="auto" className="module-message__link-preview__content">
@ -1264,6 +1272,9 @@ export class Message extends React.PureComponent<Props, State> {
blurHash={first.image.blurHash}
onError={this.handleImageError}
i18n={i18n}
showMediaNoLongerAvailableToast={
showMediaNoLongerAvailableToast
}
showVisualAttachment={() => {
openLinkInWebBrowser(first.url);
}}
@ -2599,7 +2610,8 @@ export class Message extends React.PureComponent<Props, State> {
attachments &&
attachments.length > 0 &&
!isAttachmentPending &&
!isDownloaded(attachments[0])
!isDownloaded(attachments[0]) &&
isDownloadable(attachments[0])
) {
event.preventDefault();
event.stopPropagation();

View file

@ -105,6 +105,7 @@ export type PropsReduxActions = Pick<
| 'showExpiredOutgoingTapToViewToast'
| 'showLightbox'
| 'showLightboxForViewOnceMedia'
| 'showMediaNoLongerAvailableToast'
| 'showSpoiler'
| 'startConversation'
| 'viewStory'
@ -152,6 +153,7 @@ export function MessageDetail({
showExpiredOutgoingTapToViewToast,
showLightbox,
showLightboxForViewOnceMedia,
showMediaNoLongerAvailableToast,
showSpoiler,
startConversation,
theme,
@ -375,6 +377,7 @@ export function MessageDetail({
showExpiredOutgoingTapToViewToast
}
showLightbox={showLightbox}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
startConversation={startConversation}
theme={theme}
viewStory={viewStory}

View file

@ -144,6 +144,7 @@ const defaultMessageProps: TimelineMessagesProps = {
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'),
showLightbox: action('default--showLightbox'),

View file

@ -316,6 +316,7 @@ const actions = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),

View file

@ -107,6 +107,7 @@ const getDefaultProps = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showSpoiler: action('showSpoiler'),
startConversation: action('startConversation'),

View file

@ -351,6 +351,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
showLightbox: action('showLightbox'),
@ -2198,3 +2199,129 @@ export function MultiSelect(): JSX.Element {
MultiSelect.args = {
name: 'Multi Select',
};
export function PermanentlyUndownloadableAttachments(): JSX.Element {
const imageProps = createProps({
attachments: [
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'bird.jpg',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 296,
height: 394,
path: undefined,
key: undefined,
id: undefined,
}),
],
status: 'sent',
});
const multipleImagesProps = createProps({
attachments: [
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'bird.jpg',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 296,
height: 394,
path: undefined,
key: undefined,
id: undefined,
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'bird.jpg',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 296,
height: 394,
path: undefined,
key: undefined,
id: undefined,
}),
],
status: 'sent',
});
const gifProps = createProps({
attachments: [
fakeAttachment({
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'bird.gif',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 296,
height: 394,
path: undefined,
key: undefined,
id: undefined,
}),
],
status: 'sent',
text: 'cool gif',
});
const videoProps = createProps({
attachments: [
fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'bird.mp4',
width: 720,
height: 480,
path: undefined,
key: undefined,
id: undefined,
}),
],
status: 'sent',
});
const outgoingAuthor = {
...imageProps.author,
id: getDefaultConversation().id,
};
return (
<>
<TimelineMessage {...imageProps} shouldCollapseAbove />
<TimelineMessage {...gifProps} />
<TimelineMessage {...videoProps} />
<TimelineMessage {...multipleImagesProps} shouldCollapseBelow />
<TimelineMessage
{...imageProps}
author={outgoingAuthor}
direction="outgoing"
shouldCollapseAbove
/>
<TimelineMessage
{...gifProps}
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...videoProps}
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...multipleImagesProps}
author={outgoingAuthor}
direction="outgoing"
shouldCollapseBelow
/>
</>
);
}
export const AttachmentWithError = Template.bind({});
AttachmentWithError.args = {
attachments: [
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'test.png',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 296,
height: 394,
path: undefined,
error: true,
}),
],
status: 'sent',
};

View file

@ -0,0 +1,19 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useCallback } from 'react';
export function useUndownloadableMediaHandler(
showMediaNoLongerAvailableToast: (() => void) | undefined
): (event: React.MouseEvent) => void {
return useCallback(
(event: React.MouseEvent) => {
if (showMediaNoLongerAvailableToast) {
event.preventDefault();
event.stopPropagation();
showMediaNoLongerAvailableToast();
}
},
[showMediaNoLongerAvailableToast]
);
}

View file

@ -1209,6 +1209,7 @@ export const actions = {
showFindByUsername,
showFindByPhoneNumber,
showInbox,
showMediaNoLongerAvailableToast,
startComposing,
startConversation,
startSettingGroupMetadata,
@ -4563,6 +4564,14 @@ function showInbox(): ShowInboxActionType {
payload: null,
};
}
function showMediaNoLongerAvailableToast(): ShowToastActionType {
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.MediaNoLongerAvailable,
},
};
}
type ShowConversationArgsType = ReadonlyDeep<{
conversationId?: string;

View file

@ -66,11 +66,7 @@ import type {
AttachmentForUIType,
AttachmentType,
} from '../../types/Attachment';
import {
isVoiceMessage,
canBeDownloaded,
defaultBlurHash,
} from '../../types/Attachment';
import { isVoiceMessage, defaultBlurHash } from '../../types/Attachment';
import { type DefaultConversationColorType } from '../../types/Colors';
import { ReadStatus } from '../../messages/MessageReadStatus';
@ -325,7 +321,6 @@ export const getAttachmentsForMessage = ({
}
return (
attachments
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
// Long message attachments are removed from message.attachments quickly,
// but in case they are still around, let's make sure not to show them
.filter(attachment => attachment.contentType !== LONG_MESSAGE)

View file

@ -55,6 +55,7 @@ export const SmartMessageDetail = memo(
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showMediaNoLongerAvailableToast,
showSpoiler,
startConversation,
} = useConversationsActions();
@ -115,6 +116,7 @@ export const SmartMessageDetail = memo(
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showSpoiler={showSpoiler}
startConversation={startConversation}
theme={theme}

View file

@ -133,6 +133,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showMediaNoLongerAvailableToast,
showSpoiler,
startConversation,
targetMessage,
@ -236,6 +237,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showSpoiler={showSpoiler}
startConversation={startConversation}
toggleDeleteMessagesModal={toggleDeleteMessagesModal}

View file

@ -44,6 +44,7 @@ export enum ToastType {
LinkCopied = 'LinkCopied',
LoadingFullLogs = 'LoadingFullLogs',
MaxAttachments = 'MaxAttachments',
MediaNoLongerAvailable = 'MediaNoLongerAvailable',
MessageBodyTooLong = 'MessageBodyTooLong',
MessageLoop = 'MessageLoop',
OriginalMessageNotFound = 'OriginalMessageNotFound',
@ -136,6 +137,7 @@ export type AnyToast =
| { toastType: ToastType.LinkCopied }
| { toastType: ToastType.LoadingFullLogs }
| { toastType: ToastType.MaxAttachments }
| { toastType: ToastType.MediaNoLongerAvailable }
| { toastType: ToastType.MessageBodyTooLong }
| { toastType: ToastType.MessageLoop }
| { toastType: ToastType.OriginalMessageNotFound }

View file

@ -5,6 +5,8 @@ export const PRODUCTION_DOWNLOAD_URL = 'https://signal.org/download/';
export const BETA_DOWNLOAD_URL = 'https://support.signal.org/beta';
export const UNSUPPORTED_OS_URL =
'https://support.signal.org/hc/articles/5109141421850';
export const LINKED_DEVICES_URL =
'https://support.signal.org/hc/en-us/articles/360007320551-Linked-Devices';
export const LINK_SIGNAL_DESKTOP =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
export const SAFETY_NUMBER_URL =