From 5735af59d4d5f9151f9793c56010e621b9b6c941 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 9 Sep 2023 01:12:33 +1200 Subject: [PATCH 1/2] Implement showing and sending spoilers --- src/ContentMessages.ts | 13 ++++++++- .../views/dialogs/UploadConfirmDialog.tsx | 25 ++++++++++++++--- src/components/views/messages/MImageBody.tsx | 10 ++++--- src/components/views/messages/MVideoBody.tsx | 27 ++++++++++++++++++- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 00ff644a39a..444bcc48dac 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -415,6 +415,7 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; const loopPromiseBefore = promBefore; + let shouldContentWarning = false; if (!uploadAll) { const { finished } = Modal.createDialog(UploadConfirmDialog, { @@ -422,7 +423,8 @@ export default class ContentMessages { currentIndex: i, totalFiles: okFiles.length, }); - const [shouldContinue, shouldUploadAll] = await finished; + const [shouldContinue, shouldUploadAll, contentWarning] = await finished; + shouldContentWarning = contentWarning; if (!shouldContinue) break; if (shouldUploadAll) { uploadAll = true; @@ -436,6 +438,7 @@ export default class ContentMessages { relation, matrixClient, replyToEvent ?? undefined, + shouldContentWarning, loopPromiseBefore, ), ); @@ -481,6 +484,7 @@ export default class ContentMessages { relation: IEventRelation | undefined, matrixClient: MatrixClient, replyToEvent: MatrixEvent | undefined, + contentWarning?: boolean, promBefore?: Promise, ): Promise { const fileName = file.name || _t("Attachment"); @@ -492,6 +496,13 @@ export default class ContentMessages { msgtype: MsgType.File, // set more specifically later }; + // Attach content warning + if (contentWarning) { + content["m.content_warning"] = { + type: "m.spoiler" // Since the UI checkbox is labelled "Spoiler" + } + } + // Attach mentions, which really only applies if there's a replyToEvent. attachMentions(matrixClient.getSafeUserId(), content, null, replyToEvent); attachRelation(content, relation); diff --git a/src/components/views/dialogs/UploadConfirmDialog.tsx b/src/components/views/dialogs/UploadConfirmDialog.tsx index a2203f30cbb..db5f2bf9a1c 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.tsx +++ b/src/components/views/dialogs/UploadConfirmDialog.tsx @@ -22,16 +22,21 @@ import { _t } from "../../../languageHandler"; import { getBlobSafeMimeType } from "../../../utils/blobs"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; +import StyledCheckbox from "../elements/StyledCheckbox"; import { fileSize } from "../../../utils/FileUtils"; interface IProps { file: File; currentIndex: number; totalFiles: number; - onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void; + onFinished: (uploadConfirmed: boolean, uploadAll?: boolean, contentWarning?: boolean) => void; } -export default class UploadConfirmDialog extends React.Component { +interface IState { + isContentWarning: boolean; +} + +export default class UploadConfirmDialog extends React.Component { private readonly objectUrl: string; private readonly mimeType: string; @@ -48,22 +53,30 @@ export default class UploadConfirmDialog extends React.Component { this.mimeType = getBlobSafeMimeType(props.file.type); const blob = new Blob([props.file], { type: this.mimeType }); this.objectUrl = URL.createObjectURL(blob); + + this.state = { + isContentWarning: false, + } } public componentWillUnmount(): void { if (this.objectUrl) URL.revokeObjectURL(this.objectUrl); } + private toggleContentWarning = (): void => { + this.setState({ isContentWarning: !this.state.isContentWarning }); + } + private onCancelClick = (): void => { this.props.onFinished(false); }; private onUploadClick = (): void => { - this.props.onFinished(true); + this.props.onFinished(true, false, this.state.isContentWarning); }; private onUploadAllClick = (): void => { - this.props.onFinished(true, true); + this.props.onFinished(true, true, this.state.isContentWarning); }; public render(): React.ReactNode { @@ -122,6 +135,10 @@ export default class UploadConfirmDialog extends React.Component { + this.toggleContentWarning()}> + Spoiler + + { imgError: false, imgLoaded: false, hover: false, - showImage: SettingsStore.getValue("showImages"), + showImage: SettingsStore.getValue("showImages") && !this.props.mxEvent.getContent()["m.content_warning"], placeholder: Placeholder.NoImage, }; } protected showImage(): void { - localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); this.setState({ showImage: true }); this.downloadImage(); } @@ -338,13 +337,16 @@ export default class MImageBody extends React.Component { this.unmounted = false; const showImage = - this.state.showImage || localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true"; + this.state.showImage; if (showImage) { // noinspection JSIgnoredPromiseFromCall this.downloadImage(); this.setState({ showImage: true }); - } // else don't download anything because we don't want to display anything. + } else { + // don't download anything because we don't want to display anything. + this.setState({ contentUrl: this.getContentUrl() }); // doing this ensures wrapImage() gets called later, which adds the needed onClick handler + } // Add a 150ms timer for blurhash to first appear. if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) { diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index ad9d083f083..a99018f2ae4 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -26,6 +26,7 @@ import { BLURHASH_FIELD } from "../../../utils/image-media"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IBodyProps } from "./IBodyProps"; import MFileBody from "./MFileBody"; +import { HiddenImagePlaceholder } from "./MImageBody"; import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import MediaProcessingError from "./shared/MediaProcessingError"; @@ -38,6 +39,7 @@ interface IState { fetchingData: boolean; posterLoading: boolean; blurhashUrl: string | null; + showVideo: boolean; } export default class MVideoBody extends React.PureComponent { @@ -58,6 +60,7 @@ export default class MVideoBody extends React.PureComponent error: null, posterLoading: false, blurhashUrl: null, + showVideo: true, }; } @@ -174,6 +177,7 @@ export default class MVideoBody extends React.PureComponent decryptedUrl: `data:${mimetype},`, decryptedThumbnailUrl: thumbnailUrl || `data:${mimetype},`, decryptedBlob: null, + showVideo: !content?.["m.content_warning"], }); } } catch (err) { @@ -183,6 +187,11 @@ export default class MVideoBody extends React.PureComponent error: err, }); } + } else { // not encrypted + const content = this.props.mxEvent.getContent(); + this.setState({ + showVideo: !content?.["m.content_warning"], + }) } } @@ -232,6 +241,19 @@ export default class MVideoBody extends React.PureComponent return this.showFileBody && ; }; + protected showVideo(): void { + this.setState({ showVideo: true }); + } + + protected onClick = (ev: React.MouseEvent): void => { + if (ev.button === 0 && !ev.metaKey) { + if (!this.state.showVideo) { + this.showVideo(); + ev.preventDefault(); + } + } + } + public render(): React.ReactNode { const content = this.props.mxEvent.getContent(); const autoplay = SettingsStore.getValue("autoplayVideo"); @@ -287,7 +309,8 @@ export default class MVideoBody extends React.PureComponent const fileBody = this.getFileBody(); return ( -
+
this.onClick(ev)} style={{ maxWidth, maxHeight, aspectRatio }}> + {this.state.showVideo ?
{fileBody} From 3fb8e2b204eaaa8f359d7561bee8ec8ee1cffe31 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 9 Sep 2023 01:26:35 +1200 Subject: [PATCH 2/2] Switch from m.content_warning to town.robin.msc3725.content_warning --- src/ContentMessages.ts | 2 +- src/components/views/messages/MImageBody.tsx | 2 +- src/components/views/messages/MVideoBody.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 444bcc48dac..f8fbcdfeff7 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -498,7 +498,7 @@ export default class ContentMessages { // Attach content warning if (contentWarning) { - content["m.content_warning"] = { + content["town.robin.msc3725.content_warning"] = { type: "m.spoiler" // Since the UI checkbox is labelled "Spoiler" } } diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 92477ec2bfe..78640a453af 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -83,7 +83,7 @@ export default class MImageBody extends React.Component { imgError: false, imgLoaded: false, hover: false, - showImage: SettingsStore.getValue("showImages") && !this.props.mxEvent.getContent()["m.content_warning"], + showImage: SettingsStore.getValue("showImages") && !this.props.mxEvent.getContent()["town.robin.msc3725.content_warning"], placeholder: Placeholder.NoImage, }; } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index a99018f2ae4..e08cae1bd83 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -177,7 +177,7 @@ export default class MVideoBody extends React.PureComponent decryptedUrl: `data:${mimetype},`, decryptedThumbnailUrl: thumbnailUrl || `data:${mimetype},`, decryptedBlob: null, - showVideo: !content?.["m.content_warning"], + showVideo: !content?.["town.robin.msc3725.content_warning"], }); } } catch (err) { @@ -190,7 +190,7 @@ export default class MVideoBody extends React.PureComponent } else { // not encrypted const content = this.props.mxEvent.getContent(); this.setState({ - showVideo: !content?.["m.content_warning"], + showVideo: !content?.["town.robin.msc3725.content_warning"], }) } }