diff --git a/CHANGELOG.md b/CHANGELOG.md index 827c282aae..2c99e83375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -170,6 +170,7 @@ Also make opening devtools with F12 more reliable. - Show thumbnail in chatlist summary of image, sticker and webxdc messages - add webxdc api `sendToChat` #3240 - add webxdc api `importFiles` +- add context menu option to delete media-messages from gallery ### Changed - exclude more unused files from installation package diff --git a/scss/gallery/_media-attachment.scss b/scss/gallery/_media-attachment.scss index ff8706de2e..b01e03e598 100644 --- a/scss/gallery/_media-attachment.scss +++ b/scss/gallery/_media-attachment.scss @@ -71,6 +71,12 @@ color: grey; } } + + &.broken { + .name { + color: var(--colorDanger); + } + } } .media-attachment-generic { @@ -145,6 +151,16 @@ margin-top: 3px; } } + + &.broken { + & > .file-icon { + cursor: unset; + } + .name, + .file-extension { + color: var(--colorDanger); + } + } } .media-attachment-webxdc { @@ -195,4 +211,13 @@ word-break: break-all; } } + + &.broken { + & > .icon { + background-color: var(--colorDanger); + } + .name { + color: var(--colorDanger); + } + } } diff --git a/src/renderer/components/Gallery.tsx b/src/renderer/components/Gallery.tsx index e0e95d216a..20b5c3fa06 100644 --- a/src/renderer/components/Gallery.tsx +++ b/src/renderer/components/Gallery.tsx @@ -1,6 +1,13 @@ import React, { Component } from 'react' import { ScreenContext } from '../contexts' -import MediaAttachment from './attachment/mediaAttachment' +import { + AudioAttachment, + FileAttachment, + GalleryAttachmentElementProps, + ImageAttachment, + VideoAttachment, + WebxdcAttachment, +} from './attachment/mediaAttachment' import { getLogger } from '../../shared/logger' import { BackendRemote, Type } from '../backend-com' import { selectedAccountId } from '../ScreenController' @@ -11,23 +18,31 @@ type MediaTabKey = 'images' | 'video' | 'audio' | 'files' | 'webxdc_apps' const MediaTabs: Readonly< { - [key in MediaTabKey]: { values: Type.Viewtype[] } + [key in MediaTabKey]: { + values: Type.Viewtype[] + element: (props: GalleryAttachmentElementProps) => JSX.Element + } } > = { images: { values: ['Gif', 'Image'], + element: ImageAttachment, }, video: { values: ['Video'], + element: VideoAttachment, }, audio: { values: ['Audio', 'Voice'], + element: AudioAttachment, }, files: { values: ['File'], + element: FileAttachment, }, webxdc_apps: { values: ['Webxdc'], + element: WebxdcAttachment, }, } @@ -38,8 +53,9 @@ export default class Gallery extends Component< { id: MediaTabKey msgTypes: Type.Viewtype[] - medias: Type.Message[] - errors: { msgId: number; error: string }[] + element: (props: GalleryAttachmentElementProps) => JSX.Element + mediaMessageIds: number[] + mediaLoadResult: Record } > { constructor(props: mediaProps) { @@ -47,8 +63,9 @@ export default class Gallery extends Component< this.state = { id: 'images', msgTypes: MediaTabs.images.values, - medias: [], - errors: [], + element: ImageAttachment, + mediaMessageIds: [], + mediaLoadResult: {}, } } @@ -67,29 +84,25 @@ export default class Gallery extends Component< throw new Error('chat id missing') } const msgTypes = MediaTabs[id].values + const newElement = MediaTabs[id].element const accountId = selectedAccountId() const chatId = this.props.chatId !== 'all' ? this.props.chatId : null BackendRemote.rpc .getChatMedia(accountId, chatId, msgTypes[0], msgTypes[1], null) .then(async media_ids => { - // throws if some media is not found - const all_media_fetch_results = await BackendRemote.rpc.getMessages( + const mediaLoadResult = await BackendRemote.rpc.getMessages( accountId, media_ids ) - const medias: Type.Message[] = [] - const errors = [] - for (const msgId of media_ids) { - const result = all_media_fetch_results[msgId] - if (result.variant === 'message') { - medias.push(result) - } else { - errors.push({ msgId, error: result.error }) - } - } - log.errorWithoutStackTrace('messages failed to load:', errors) - this.setState({ id, msgTypes, medias, errors }) + media_ids.reverse() // order newest up - if we need different ordering we need to do it in core + this.setState({ + id, + msgTypes, + element: newElement, + mediaMessageIds: media_ids, + mediaLoadResult, + }) this.forceUpdate() }) .catch(log.error.bind(log)) @@ -113,7 +126,7 @@ export default class Gallery extends Component< } render() { - const { medias, id, errors } = this.state + const { mediaMessageIds, mediaLoadResult, id } = this.state const tx = window.static_translate // static because dynamic isn't too important here const emptyTabMessage = this.emptyTabMessage(id) @@ -138,39 +151,20 @@ export default class Gallery extends Component<
- {errors.length > 0 && ( -
- The following messages failed to load, please report these - errors to the developers: -
    - {errors.map(error => ( -
  • - {error.msgId} {'->'} {error.error} -
  • - ))} -
-
- )} -
- {medias.length < 1 ? ( +
+ {mediaMessageIds.length < 1 ? (

{emptyTabMessage}

) : ( '' )} - {medias - .sort((a, b) => b.sortTimestamp - a.sortTimestamp) - .map(message => { - return ( -
- -
- ) - })} + {mediaMessageIds.map(msgId => { + const message = mediaLoadResult[msgId] + return ( +
+ +
+ ) + })}
diff --git a/src/renderer/components/attachment/mediaAttachment.tsx b/src/renderer/components/attachment/mediaAttachment.tsx index 0ff79ec172..6b49647e13 100644 --- a/src/renderer/components/attachment/mediaAttachment.tsx +++ b/src/renderer/components/attachment/mediaAttachment.tsx @@ -3,8 +3,9 @@ import { openAttachmentInShell, onDownload, openWebxdc, + confirmDeleteMessage, } from '../message/messageFunctions' -import { ScreenContext } from '../../contexts' +import { ScreenContext, unwrapContext } from '../../contexts' import { isImage, isVideo, @@ -18,39 +19,15 @@ import { OpenDialogFunctionType } from '../dialogs/DialogController' import { runtime } from '../../runtime' import filesizeConverter from 'filesize' -import { jumpToMessage } from '../helpers/ChatMethods' +import { deleteMessage, jumpToMessage } from '../helpers/ChatMethods' import { getLogger } from '../../../shared/logger' import { truncateText } from '../../../shared/util' import { Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' +import ConfirmationDialog from '../dialogs/ConfirmationDialog' const log = getLogger('mediaAttachment') -export default function MediaAttachment({ - message, -}: { - message: Type.Message -}) { - if (!message.file) { - return null - } - switch (message.viewType) { - case 'Gif': - case 'Image': - return - case 'Video': - return - case 'Audio': - case 'Voice': - return - case 'Webxdc': - return - case 'File': - default: - return - } -} - const hideOpenInShellTypes: Type.Viewtype[] = [ 'Gif', 'Image', @@ -94,12 +71,23 @@ const contextMenuFactory = ( label: tx('menu_message_details'), action: openDialog.bind(null, 'MessageDetail', { id: msgId }), }, + { + label: tx('delete'), + action: () => + openDialog(ConfirmationDialog, { + message: tx('ask_delete_message'), + confirmLabel: tx('delete'), + cb: (yes: boolean) => yes && deleteMessage(msgId), + }), + }, ] } /** provides a quick link to comonly used functions to save a few duplicated lines */ -const useMediaActions = (message: Type.Message) => { - const { openDialog, openContextMenu } = useContext(ScreenContext) +const getMediaActions = ( + { openDialog, openContextMenu }: unwrapContext, + message: Type.Message +) => { return { openContextMenu: makeContextMenu( contextMenuFactory.bind(null, message, openDialog), @@ -113,6 +101,27 @@ const useMediaActions = (message: Type.Message) => { } } +function getBrokenMediaContextMenu( + { openContextMenu, openDialog }: unwrapContext, + msgId: number +) { + const tx = window.static_translate + return makeContextMenu( + [ + { + label: tx('delete'), + action: () => + openDialog(ConfirmationDialog, { + message: tx('ask_delete_message'), + confirmLabel: tx('delete'), + cb: (yes: boolean) => yes && deleteMessage(msgId), + }), + }, + ], + openContextMenu + ) +} + function squareBrokenMediaContent( hasSupportedFormat: boolean, contentType: string | null @@ -127,159 +136,310 @@ function squareBrokenMediaContent( ) } -function ImageAttachment({ message }: { message: Type.Message }) { - const { openContextMenu, openFullscreenMedia, openInShell } = useMediaActions( - message - ) - const { file, fileMime } = message - const hasSupportedFormat = isImage(fileMime || '') - const isBroken = !file || !hasSupportedFormat - return ( -
- {isBroken ? ( - squareBrokenMediaContent(hasSupportedFormat, fileMime || '') - ) : ( - - )} -
- ) +export type GalleryAttachmentElementProps = { + msgId: number + load_result: Type.MessageLoadResult } -function VideoAttachment({ message }: { message: Type.Message }) { - const { openContextMenu, openFullscreenMedia, openInShell } = useMediaActions( - message - ) - const { fileMime, file } = message - const hasSupportedFormat = isVideo(fileMime) - const isBroken = !file || !hasSupportedFormat - return ( -
- {isBroken ? ( - squareBrokenMediaContent(hasSupportedFormat, fileMime) - ) : ( - <> -
+ ) + } else { + const message = load_result + const { + openContextMenu, + openFullscreenMedia, + openInShell, + } = getMediaActions(screenContext, message) + const { file, fileMime } = message + const hasSupportedFormat = isImage(fileMime) + const isBroken = !file || !hasSupportedFormat + return ( +
+ {isBroken ? ( + squareBrokenMediaContent(hasSupportedFormat, fileMime) + ) : ( + -
-
-
- - )} -
- ) + )} +
+ ) + } } -function AudioAttachment({ message }: { message: Type.Message }) { - const { openContextMenu } = useMediaActions(message) - const { fileMime, file } = message - const hasSupportedFormat = isAudio(fileMime) - return ( -
-
-
{message?.sender.displayName}
- -
- {hasSupportedFormat ? ( - - ) : ( -
- {window.static_translate( - 'cannot_display_unsuported_file_type', - fileMime || 'null' - )} +export function VideoAttachment({ + msgId, + load_result, +}: GalleryAttachmentElementProps) { + const screenContext = useContext(ScreenContext) + const tx = window.static_translate + + if (load_result.variant === 'loadingError') { + const onContextMenu = getBrokenMediaContextMenu(screenContext, msgId) + return ( +
+
+ {tx('attachment_failed_to_load')}
- )} -
- ) +
+ ) + } else { + const message = load_result + const { + openContextMenu, + openFullscreenMedia, + openInShell, + } = getMediaActions(screenContext, message) + const { file, fileMime } = message + const hasSupportedFormat = isVideo(fileMime) + const isBroken = !file || !hasSupportedFormat + return ( +
+ {isBroken ? ( + squareBrokenMediaContent(hasSupportedFormat, fileMime || '') + ) : ( + <> +