Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement showing and sending media spoilers #20

Open
wants to merge 2 commits into
base: sc
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/ContentMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@
const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent();
if (!this.mediaConfig) {
// hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");

Check failure on line 380 in src/ContentMessages.ts

View workflow job for this annotation

GitHub Actions / Typescript Strict Error Checker (--strict --noImplicitAny)

Argument of type 'typeof Spinner' is not assignable to parameter of type 'ComponentType'.

src/ContentMessages.ts:380:46 - Argument of type 'typeof Spinner' is not assignable to parameter of type 'ComponentType'.
await Promise.race([this.ensureMediaConfigFetched(matrixClient), modal.finished]);
if (!this.mediaConfig) {
// User cancelled by clicking away on the spinner
Expand Down Expand Up @@ -415,14 +415,16 @@
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, {
file,
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;
Expand All @@ -436,6 +438,7 @@
relation,
matrixClient,
replyToEvent ?? undefined,
shouldContentWarning,
loopPromiseBefore,
),
);
Expand Down Expand Up @@ -481,6 +484,7 @@
relation: IEventRelation | undefined,
matrixClient: MatrixClient,
replyToEvent: MatrixEvent | undefined,
contentWarning?: boolean,
promBefore?: Promise<any>,
): Promise<void> {
const fileName = file.name || _t("Attachment");
Expand All @@ -492,6 +496,13 @@
msgtype: MsgType.File, // set more specifically later
};

// Attach content warning
if (contentWarning) {
content["town.robin.msc3725.content_warning"] = {

Check failure on line 501 in src/ContentMessages.ts

View workflow job for this annotation

GitHub Actions / Typescript Strict Error Checker (--strict --noImplicitAny)

Property 'town.robin.msc3725.content_warning' does not exist on type 'Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo>; }'.

src/ContentMessages.ts:501:13 - Element implicitly has an 'any' type because expression of type '"town.robin.msc3725.content_warning"' can't be used to index type 'Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo>; }'.

Check failure on line 501 in src/ContentMessages.ts

View workflow job for this annotation

GitHub Actions / Typescript Strict Error Checker (--noImplicitAny)

Property 'town.robin.msc3725.content_warning' does not exist on type 'Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo>; }'.

src/ContentMessages.ts:501:13 - Element implicitly has an 'any' type because expression of type '"town.robin.msc3725.content_warning"' can't be used to index type 'Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo>; }'.
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);
Expand Down Expand Up @@ -580,7 +591,7 @@
fileName: upload.fileName,
});
}
Modal.createDialog(ErrorDialog, {

Check failure on line 594 in src/ContentMessages.ts

View workflow job for this annotation

GitHub Actions / Typescript Strict Error Checker (--strict --noImplicitAny)

Argument of type 'typeof ErrorDialog' is not assignable to parameter of type 'ComponentType'.

src/ContentMessages.ts:594:36 - Argument of type 'typeof ErrorDialog' is not assignable to parameter of type 'ComponentType'.
title: _t("Upload Failed"),
description: desc,
});
Expand Down
25 changes: 21 additions & 4 deletions src/components/views/dialogs/UploadConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IProps> {
interface IState {
isContentWarning: boolean;
}

export default class UploadConfirmDialog extends React.Component<IProps, IState> {
private readonly objectUrl: string;
private readonly mimeType: string;

Expand All @@ -48,22 +53,30 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
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 {
Expand Down Expand Up @@ -122,6 +135,10 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
</div>
</div>

<StyledCheckbox checked={this.state.isContentWarning} onChange={() => this.toggleContentWarning()}>
Spoiler
</StyledCheckbox>

<DialogButtons
primaryButton={_t("Upload")}
hasCancel={false}
Expand Down
10 changes: 6 additions & 4 deletions src/components/views/messages/MImageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imgError: false,
imgLoaded: false,
hover: false,
showImage: SettingsStore.getValue("showImages"),
showImage: SettingsStore.getValue("showImages") && !this.props.mxEvent.getContent()["town.robin.msc3725.content_warning"],
placeholder: Placeholder.NoImage,
};
}

protected showImage(): void {
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
this.setState({ showImage: true });
this.downloadImage();
}
Expand Down Expand Up @@ -338,13 +337,16 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
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]) {
Expand Down
27 changes: 26 additions & 1 deletion src/components/views/messages/MVideoBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -38,6 +39,7 @@ interface IState {
fetchingData: boolean;
posterLoading: boolean;
blurhashUrl: string | null;
showVideo: boolean;
}

export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
Expand All @@ -58,6 +60,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
error: null,
posterLoading: false,
blurhashUrl: null,
showVideo: true,
};
}

Expand Down Expand Up @@ -174,6 +177,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
decryptedUrl: `data:${mimetype},`,
decryptedThumbnailUrl: thumbnailUrl || `data:${mimetype},`,
decryptedBlob: null,
showVideo: !content?.["town.robin.msc3725.content_warning"],
});
}
} catch (err) {
Expand All @@ -183,6 +187,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
error: err,
});
}
} else { // not encrypted
const content = this.props.mxEvent.getContent<IMediaEventContent>();
this.setState({
showVideo: !content?.["town.robin.msc3725.content_warning"],
})
}
}

Expand Down Expand Up @@ -232,6 +241,19 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
return this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />;
};

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");
Expand Down Expand Up @@ -287,7 +309,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
const fileBody = this.getFileBody();
return (
<span className="mx_MVideoBody">
<div className="mx_MVideoBody_container" style={{ maxWidth, maxHeight, aspectRatio }}>
<div className="mx_MVideoBody_container" onClick={ev => this.onClick(ev)} style={{ maxWidth, maxHeight, aspectRatio }}>
{this.state.showVideo ?
<video
className="mx_MVideoBody"
ref={this.videoRef}
Expand All @@ -303,6 +326,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
poster={poster}
onPlay={this.videoOnPlay}
/>
:
<HiddenImagePlaceholder />}
{spaceFiller}
</div>
{fileBody}
Expand Down
Loading