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 @@ -415,14 +415,16 @@ 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, {
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 @@ export default class ContentMessages {
relation,
matrixClient,
replyToEvent ?? undefined,
shouldContentWarning,
loopPromiseBefore,
),
);
Expand Down Expand Up @@ -481,6 +484,7 @@ export default class ContentMessages {
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 @@ export default class ContentMessages {
msgtype: MsgType.File, // set more specifically later
};

// Attach content warning
if (contentWarning) {
content["town.robin.msc3725.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);
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