mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(app): delete attachments from the media viewer (#2039)
This commit is contained in:
1 parent
88d85919f1
commit
509562e6da
11 files changed
+92
-11
No files matched your search
+2
-1
@@ -208,9 +208,10 @@ export const AttachmentGridItem: FC<AttachmentGridItemProps> = observer(
|
||||
messageId: message?.id,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete: !isPreview && snapshotIndex === undefined,
|
||||
});
|
||||
},
|
||||
[attachment, message, messageViewContext?.channel, mediaAttachments, shouldBlur],
|
||||
[attachment, message, messageViewContext?.channel, mediaAttachments, shouldBlur, isPreview, snapshotIndex],
|
||||
);
|
||||
const openInBrowser = useOpenInBrowserOnMiddleClick(attachment.url ?? attachment.proxy_url ?? '', !shouldBlur);
|
||||
const handleFavoriteClick = useCallback(
|
||||
|
||||
@@ -181,6 +181,7 @@ const useImagePreview = ({
|
||||
message,
|
||||
sourceChannel,
|
||||
providerName,
|
||||
allowAttachmentDelete = false,
|
||||
}: {
|
||||
proxyUrl: string;
|
||||
embedUrl: string;
|
||||
@@ -195,6 +196,7 @@ const useImagePreview = ({
|
||||
message?: Message;
|
||||
sourceChannel?: Channel | null;
|
||||
providerName?: string;
|
||||
allowAttachmentDelete?: boolean;
|
||||
}): {viewerItem: MediaViewerItem; openPreview: (event: React.MouseEvent | React.KeyboardEvent) => void} => {
|
||||
const viewerItem = useMemo<MediaViewerItem>(
|
||||
() => ({
|
||||
@@ -223,9 +225,10 @@ const useImagePreview = ({
|
||||
messageId,
|
||||
message,
|
||||
sourceChannel,
|
||||
allowAttachmentDelete,
|
||||
});
|
||||
},
|
||||
[viewerItem, channelId, messageId, message, sourceChannel],
|
||||
[viewerItem, channelId, messageId, message, sourceChannel, allowAttachmentDelete],
|
||||
);
|
||||
return {viewerItem, openPreview};
|
||||
};
|
||||
@@ -461,6 +464,7 @@ export const EmbedGifv: FC<
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
providerName,
|
||||
allowAttachmentDelete: !isPreview && snapshotIndex === undefined,
|
||||
});
|
||||
const handleDeleteClick = useDeleteAttachment(message, attachmentId);
|
||||
const handleDownloadClick = useCallback(
|
||||
@@ -795,6 +799,7 @@ export const EmbedGif: FC<
|
||||
contentHash,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete: !isPreview && snapshotIndex === undefined,
|
||||
});
|
||||
const {scheduleViewerWarm, cancelViewerWarm} = useMediaViewerHoverWarm(viewerItem, {
|
||||
allowAnimated: gifAutoPlay,
|
||||
|
||||
@@ -104,6 +104,7 @@ interface ImagePreviewHandlerProps {
|
||||
message?: Message;
|
||||
animated?: boolean;
|
||||
mediaAttachments?: ReadonlyArray<MessageAttachment>;
|
||||
allowAttachmentDelete?: boolean;
|
||||
onViewerWarmEnter?: () => void;
|
||||
onViewerWarmLeave?: () => void;
|
||||
children: React.ReactNode;
|
||||
@@ -143,6 +144,7 @@ const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
|
||||
message,
|
||||
animated,
|
||||
mediaAttachments = NO_MEDIA_ATTACHMENTS,
|
||||
allowAttachmentDelete = false,
|
||||
onViewerWarmEnter,
|
||||
onViewerWarmLeave,
|
||||
children,
|
||||
@@ -176,6 +178,7 @@ const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
});
|
||||
} else {
|
||||
MediaViewerCommands.openMediaViewer(
|
||||
@@ -187,6 +190,7 @@ const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
|
||||
naturalHeight,
|
||||
type: 'image' as const,
|
||||
contentHash,
|
||||
attachmentId,
|
||||
embedIndex,
|
||||
expiresAt: undefined,
|
||||
expired: undefined,
|
||||
@@ -199,6 +203,7 @@ const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -217,6 +222,7 @@ const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
|
||||
message,
|
||||
messageViewContext?.channel,
|
||||
mediaAttachments,
|
||||
allowAttachmentDelete,
|
||||
],
|
||||
);
|
||||
const openInBrowser = useOpenInBrowserOnMiddleClick(originalSrc || src);
|
||||
@@ -381,6 +387,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
|
||||
[originalSrc, src],
|
||||
);
|
||||
const handleDeleteClick = useDeleteAttachment(message, attachmentId);
|
||||
const allowAttachmentDelete = !isPreview && snapshotIndex === undefined;
|
||||
const [mediaSheetOpen, setMediaSheetOpen] = useState(false);
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -524,6 +531,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
|
||||
attachmentId={attachmentId}
|
||||
message={message}
|
||||
mediaAttachments={mediaAttachments}
|
||||
allowAttachmentDelete={allowAttachmentDelete}
|
||||
animated={animated}
|
||||
onViewerWarmEnter={scheduleViewerWarm}
|
||||
onViewerWarmLeave={cancelViewerWarm}
|
||||
|
||||
@@ -272,6 +272,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
[src],
|
||||
);
|
||||
const handleDeleteClick = useDeleteAttachment(message, attachmentId);
|
||||
const allowAttachmentDelete = !isPreview && snapshotIndex === undefined;
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!message) return;
|
||||
@@ -396,6 +397,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
});
|
||||
} else {
|
||||
MediaViewerCommands.openMediaViewer(
|
||||
@@ -407,12 +409,13 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
naturalHeight: viewerVideoDimensions.height,
|
||||
type: 'video' as const,
|
||||
contentHash,
|
||||
attachmentId,
|
||||
embedIndex,
|
||||
duration,
|
||||
},
|
||||
],
|
||||
0,
|
||||
{channelId, messageId, message, sourceChannel: messageViewContext?.channel},
|
||||
{channelId, messageId, message, sourceChannel: messageViewContext?.channel, allowAttachmentDelete},
|
||||
);
|
||||
}
|
||||
}, [
|
||||
@@ -420,6 +423,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
mediaAttachments,
|
||||
attachmentId,
|
||||
effectiveSrc,
|
||||
@@ -451,6 +455,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
sourceChannel: messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
});
|
||||
} else {
|
||||
MediaViewerCommands.openMediaViewer(
|
||||
@@ -462,13 +467,14 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
naturalHeight: viewerVideoDimensions.height,
|
||||
type: 'video' as const,
|
||||
contentHash,
|
||||
attachmentId,
|
||||
embedIndex,
|
||||
duration,
|
||||
initialTime: currentTime,
|
||||
},
|
||||
],
|
||||
0,
|
||||
{channelId, messageId, message, sourceChannel: messageViewContext?.channel},
|
||||
{channelId, messageId, message, sourceChannel: messageViewContext?.channel, allowAttachmentDelete},
|
||||
);
|
||||
}
|
||||
setIsPlayingInline(false);
|
||||
@@ -477,6 +483,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
messageId,
|
||||
message,
|
||||
messageViewContext?.channel,
|
||||
allowAttachmentDelete,
|
||||
mediaAttachments,
|
||||
attachmentId,
|
||||
effectiveSrc,
|
||||
|
||||
@@ -95,6 +95,7 @@ export const MediaModal: FC<MediaModalProps> = observer(
|
||||
onOpenInBrowser,
|
||||
onCopyLink,
|
||||
onCopyMedia,
|
||||
onDeleteAttachment,
|
||||
onReply,
|
||||
onForward,
|
||||
children,
|
||||
@@ -636,6 +637,7 @@ export const MediaModal: FC<MediaModalProps> = observer(
|
||||
onOpenInBrowser={onOpenInBrowser}
|
||||
onCopyLink={onCopyLink}
|
||||
onCopyMedia={onCopyMedia}
|
||||
onDeleteAttachment={onDeleteAttachment}
|
||||
onReset={handleResetMedia}
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import {useBottomSheetBackHandler} from '@app/features/app/hooks/useBottomSheetBackHandler';
|
||||
import {deriveDefaultNameFromMessage} from '@app/features/channel/components/embeds/EmbedUtils';
|
||||
import {useMessageActionMenuData} from '@app/features/channel/components/MessageActionMenu';
|
||||
@@ -14,6 +15,7 @@ import type {ForwardModalSuccess} from '@app/features/messaging/components/modal
|
||||
import {MediaModal} from '@app/features/messaging/components/modals/MediaModal';
|
||||
import styles from '@app/features/messaging/components/modals/MediaViewerModal.module.css';
|
||||
import {getMediaViewerPortalRoot} from '@app/features/messaging/components/modals/MediaViewerPortal';
|
||||
import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttachment';
|
||||
import {useMediaFavorite} from '@app/features/messaging/hooks/useMediaFavorite';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {formatAttachmentDate} from '@app/features/messaging/utils/AttachmentExpiryUtils';
|
||||
@@ -273,7 +275,8 @@ const MobileMediaOptionsSheet: FC<MobileMediaOptionsSheetProps> = observer(funct
|
||||
});
|
||||
const MediaViewerModalComponent: FC = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
const {isOpen, items, currentIndex, channelId, messageId, message, sourceChannel} = MediaViewer;
|
||||
const {isOpen, items, currentIndex, channelId, messageId, message, sourceChannel, allowAttachmentDelete} =
|
||||
MediaViewer;
|
||||
const {enabled: isMobile} = MobileLayout;
|
||||
const [isMediaMenuOpen, setIsMediaMenuOpen] = useState(false);
|
||||
const currentItem = items[currentIndex];
|
||||
@@ -438,6 +441,22 @@ const MediaViewerModalComponent: FC = observer(() => {
|
||||
() => (message ? untracked(() => getMessagePermissions(message, sourceChannel)) : null),
|
||||
[message, sourceChannel],
|
||||
);
|
||||
const deletableAttachmentId =
|
||||
Accessibility.showMediaDeleteButton &&
|
||||
allowAttachmentDelete &&
|
||||
permissions?.canDeleteAttachment &&
|
||||
currentItem?.attachmentId &&
|
||||
message?.attachments.some((attachment) => attachment.id === currentItem.attachmentId)
|
||||
? currentItem.attachmentId
|
||||
: undefined;
|
||||
const handleAttachmentDeleted = useCallback(() => {
|
||||
if (MediaViewer.isOpen && MediaViewer.items === items) {
|
||||
MediaViewerCommands.closeMediaViewer();
|
||||
}
|
||||
}, [items]);
|
||||
const handleDeleteAttachment = useDeleteAttachment(message, deletableAttachmentId, {
|
||||
onDeleted: handleAttachmentDeleted,
|
||||
});
|
||||
const forwardMediaSelection = useMemo<MessageCommands.ForwardMediaSelection | undefined>(() => {
|
||||
if (!currentItem) return undefined;
|
||||
if (currentItem.attachmentId) {
|
||||
@@ -672,6 +691,7 @@ const MediaViewerModalComponent: FC = observer(() => {
|
||||
onOpenInBrowser={handleOpenInBrowser}
|
||||
onCopyLink={handleCopyLink}
|
||||
onCopyMedia={handleCopyMedia}
|
||||
onDeleteAttachment={deletableAttachmentId ? handleDeleteAttachment : undefined}
|
||||
onReply={permissions?.canSendMessages ? handleReply : undefined}
|
||||
onForward={canForwardCurrentMedia ? handleForward : undefined}
|
||||
enablePanZoom={currentItem.type === 'image' || currentItem.type === 'gif' || currentItem.type === 'gifv'}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
ADD_TO_FAVORITES_DESCRIPTOR,
|
||||
COPY_LINK_DESCRIPTOR,
|
||||
DELETE_ATTACHMENT_DESCRIPTOR,
|
||||
REMOVE_FROM_FAVORITES_DESCRIPTOR,
|
||||
ZOOM_IN_DESCRIPTOR,
|
||||
ZOOM_OUT_DESCRIPTOR,
|
||||
@@ -36,16 +37,17 @@ import {
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {type FC, forwardRef, type ReactNode, type Ref} from 'react';
|
||||
import {type FC, forwardRef, type MouseEventHandler, type ReactNode, type Ref} from 'react';
|
||||
|
||||
interface ControlButtonProps {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
variant?: 'default' | 'primary' | 'danger';
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -119,6 +121,7 @@ interface MediaOverlayActionsProps {
|
||||
onOpenInBrowser?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
onCopyMedia?: () => void;
|
||||
onDeleteAttachment?: MouseEventHandler<HTMLButtonElement>;
|
||||
onReset?: () => void;
|
||||
onZoomIn?: () => void;
|
||||
onZoomOut?: () => void;
|
||||
@@ -144,6 +147,7 @@ export const MediaOverlayActions: FC<MediaOverlayActionsProps> = observer(
|
||||
onOpenInBrowser,
|
||||
onCopyLink,
|
||||
onCopyMedia,
|
||||
onDeleteAttachment,
|
||||
onReset,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
@@ -250,6 +254,21 @@ export const MediaOverlayActions: FC<MediaOverlayActionsProps> = observer(
|
||||
data-flx="messaging.media-modal.media-controls.media-overlay-actions.overlay-tooltip-button.download"
|
||||
/>
|
||||
)}
|
||||
{onDeleteAttachment && (
|
||||
<OverlayTooltipButton
|
||||
icon={
|
||||
<TrashIcon
|
||||
size={20}
|
||||
weight="bold"
|
||||
data-flx="messaging.media-modal.media-overlay-actions.delete-attachment-icon"
|
||||
/>
|
||||
}
|
||||
label={i18n._(DELETE_ATTACHMENT_DESCRIPTOR)}
|
||||
onClick={onDeleteAttachment}
|
||||
variant="danger"
|
||||
data-flx="messaging.media-modal.media-controls.media-overlay-actions.overlay-tooltip-button.delete-attachment"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={styles.overlayActionGap}
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import type {ReactNode} from 'react';
|
||||
import type {MouseEventHandler, ReactNode} from 'react';
|
||||
|
||||
export type ZoomState = 'fit' | 'zoomed';
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface MediaModalProps {
|
||||
onOpenInBrowser?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
onCopyMedia?: () => void;
|
||||
onDeleteAttachment?: MouseEventHandler<HTMLButtonElement>;
|
||||
onReply?: () => void;
|
||||
onForward?: () => void;
|
||||
onInfo?: () => void;
|
||||
|
||||
@@ -9,14 +9,26 @@ import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {useCallback} from 'react';
|
||||
|
||||
export function useDeleteAttachment(message: Message | null | undefined, attachmentId: string | null | undefined) {
|
||||
interface DeleteAttachmentOptions {
|
||||
onDeleted?: () => void;
|
||||
}
|
||||
|
||||
export function useDeleteAttachment(
|
||||
message: Message | null | undefined,
|
||||
attachmentId: string | null | undefined,
|
||||
options?: DeleteAttachmentOptions,
|
||||
) {
|
||||
const {i18n} = useLingui();
|
||||
const onDeleted = options?.onDeleted;
|
||||
return useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!message || !attachmentId) return;
|
||||
const deleteAttachment = () => MessageCommands.deleteAttachment(message.channelId, message.id, attachmentId);
|
||||
const deleteAttachment = async () => {
|
||||
await MessageCommands.deleteAttachment(message.channelId, message.id, attachmentId);
|
||||
onDeleted?.();
|
||||
};
|
||||
if (e.shiftKey) {
|
||||
void deleteAttachment();
|
||||
return;
|
||||
@@ -39,6 +51,6 @@ export function useDeleteAttachment(message: Message | null | undefined, attachm
|
||||
)),
|
||||
);
|
||||
},
|
||||
[message, attachmentId, i18n],
|
||||
[message, attachmentId, i18n, onDeleted],
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ interface MediaViewerOpenOptions {
|
||||
messageId?: string;
|
||||
message?: Message;
|
||||
sourceChannel?: Channel | null;
|
||||
allowAttachmentDelete?: boolean;
|
||||
}
|
||||
|
||||
type MediaViewerIntent =
|
||||
@@ -26,6 +27,7 @@ function dispatchMediaViewerIntent(intent: MediaViewerIntent): void {
|
||||
intent.options?.messageId,
|
||||
intent.options?.message,
|
||||
intent.options?.sourceChannel,
|
||||
intent.options?.allowAttachmentDelete,
|
||||
);
|
||||
return;
|
||||
case 'close':
|
||||
|
||||
@@ -36,6 +36,7 @@ class MediaViewer {
|
||||
messageId?: string = undefined;
|
||||
message?: Message = undefined;
|
||||
sourceChannel?: Channel = undefined;
|
||||
allowAttachmentDelete: boolean = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(
|
||||
@@ -56,6 +57,7 @@ class MediaViewer {
|
||||
messageId?: string,
|
||||
message?: Message,
|
||||
sourceChannel?: Channel | null,
|
||||
allowAttachmentDelete: boolean = false,
|
||||
): void {
|
||||
this.isOpen = true;
|
||||
this.items = copyMediaViewerItems(items);
|
||||
@@ -64,6 +66,7 @@ class MediaViewer {
|
||||
this.messageId = messageId;
|
||||
this.message = message;
|
||||
this.sourceChannel = sourceChannel ?? undefined;
|
||||
this.allowAttachmentDelete = allowAttachmentDelete;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
@@ -74,6 +77,7 @@ class MediaViewer {
|
||||
this.messageId = undefined;
|
||||
this.message = undefined;
|
||||
this.sourceChannel = undefined;
|
||||
this.allowAttachmentDelete = false;
|
||||
}
|
||||
|
||||
navigate(index: number): void {
|
||||
|
||||
Reference in new issue
Block a user