Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

MSC3531 - Implementing message hiding pending moderation #7518

Merged
merged 7 commits into from
Jan 17, 2022
Merged
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
2 changes: 1 addition & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ that room administrators cannot force account-only settings upon participants.
## Settings

Settings are the different options a user may set or experience in the application. These are pre-defined in
`src/settings/Settings.ts` under the `SETTINGS` constant, and match the `ISetting` interface as defined there.
`src/settings/Settings.tsx` under the `SETTINGS` constant, and match the `ISetting` interface as defined there.

Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some
settings, like the "theme" setting, are special cased in the config file):
Expand Down
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
@import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss";
@import "./views/messages/_HiddenBody.scss";
@import "./views/messages/_MEmoteBody.scss";
@import "./views/messages/_MFileBody.scss";
@import "./views/messages/_MImageBody.scss";
Expand Down
37 changes: 37 additions & 0 deletions res/css/views/messages/_HiddenBody.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_HiddenBody {
white-space: pre-wrap;
color: $muted-fg-color;
vertical-align: middle;

padding-left: 20px;
position: relative;

&::before {
height: 14px;
width: 14px;
background-color: $muted-fg-color;
mask-image: url('$(res)/img/element-icons/hide.svg');

mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
content: '';
position: absolute;
top: 1px;
left: 0;
}
}
10 changes: 10 additions & 0 deletions res/css/views/rooms/_EventTile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ $left-gutter: 64px;
cursor: pointer;
}

.mx_EventTile_content .mx_EventTile_pendingModeration {
user-select: none;
font-size: $font-12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
}

.mx_EventTile_e2eIcon {
position: relative;
width: 14px;
Expand Down Expand Up @@ -895,12 +903,14 @@ $left-gutter: 64px;
width: 100%;

.mx_EventTile_content,
.mx_HiddenBody,
.mx_RedactedBody,
.mx_ReplyChain_wrapper {
margin-left: 36px;
margin-right: 50px;

.mx_EventTile_content,
.mx_HiddenBody,
.mx_RedactedBody,
.mx_MImageBody {
margin: 0;
Expand Down
4 changes: 3 additions & 1 deletion res/css/views/rooms/_ReplyTile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ limitations under the License.
color: $primary-content;
}

.mx_RedactedBody {
.mx_RedactedBody,
.mx_HiddenBody {

padding: 4px 0 2px 20px;

&::before {
Expand Down
23 changes: 14 additions & 9 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private scrollPanel = createRef<ScrollPanel>();

private readonly showTypingNotificationsWatcherRef: string;
private eventNodes: Record<string, HTMLElement>;
private eventTiles: Record<string, EventTile> = {};

// A map of <callId, CallEventGrouper>
private callEventGroupers = new Map<string, CallEventGrouper>();
Expand Down Expand Up @@ -324,11 +324,18 @@ export default class MessagePanel extends React.Component<IProps, IState> {

/* get the DOM node representing the given event */
public getNodeForEventId(eventId: string): HTMLElement {
if (!this.eventNodes) {
if (!this.eventTiles) {
return undefined;
}

return this.eventNodes[eventId];
return this.eventTiles[eventId]?.ref?.current;
}

public getTileForEventId(eventId: string): EventTile {
if (!this.eventTiles) {
return undefined;
}
return this.eventTiles[eventId];
}

/* return true if the content is fully scrolled down right now; else false.
Expand Down Expand Up @@ -429,7 +436,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}

public scrollToEventIfNeeded(eventId: string): void {
const node = this.eventNodes[eventId];
const node = this.getNodeForEventId(eventId);
if (node) {
node.scrollIntoView({
block: "nearest",
Expand Down Expand Up @@ -584,8 +591,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}
}
private getEventTiles(): ReactNode[] {
this.eventNodes = {};

let i;

// first figure out which is the last event in the list which we're
Expand Down Expand Up @@ -776,7 +781,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
ref={this.collectEventNode.bind(this, eventId)}
ref={this.collectEventTile.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
continuation={continuation}
Expand Down Expand Up @@ -909,8 +914,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
return receiptsByEvent;
}

private collectEventNode = (eventId: string, node: EventTile): void => {
this.eventNodes[eventId] = node?.ref?.current;
private collectEventTile = (eventId: string, node: EventTile): void => {
this.eventTiles[eventId] = node;
};

// once dynamic content in the events load, make the scrollPanel check the
Expand Down
54 changes: 53 additions & 1 deletion src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@ import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timelin
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { SyncState } from 'matrix-js-sdk/src/sync';
import { RoomMember } from 'matrix-js-sdk';
import { debounce } from 'lodash';
import { logger } from "matrix-js-sdk/src/logger";

Expand Down Expand Up @@ -276,6 +277,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.timelineReset", this.onRoomTimelineReset);
cli.on("Room.redaction", this.onRoomRedaction);
if (SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) {
// Make sure that events are re-rendered when their visibility-pending-moderation changes.
cli.on("Event.visibilityChange", this.onEventVisibilityChange);
cli.on("RoomMember.powerLevel", this.onVisibilityPowerLevelChange);
}
// same event handler as Room.redaction as for both we just do forceUpdate
cli.on("Room.redactionCancelled", this.onRoomRedaction);
cli.on("Room.receipt", this.onRoomReceipt);
Expand Down Expand Up @@ -352,8 +358,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("RoomMember.powerLevel", this.onVisibilityPowerLevelChange);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Event.replaced", this.onEventReplaced);
client.removeListener("Event.visibilityChange", this.onEventVisibilityChange);
client.removeListener("sync", this.onSync);
}
}
Expand Down Expand Up @@ -619,6 +627,50 @@ class TimelinePanel extends React.Component<IProps, IState> {
this.forceUpdate();
};

// Called whenever the visibility of an event changes, as per
// MSC3531. We typically need to re-render the tile.
private onEventVisibilityChange = (ev: MatrixEvent): void => {
if (this.unmounted) {
return;
}

// ignore events for other rooms
const roomId = ev.getRoomId();
if (roomId !== this.props.timelineSet.room?.roomId) {
return;
}

// we could skip an update if the event isn't in our timeline,
// but that's probably an early optimisation.
const tile = this.messagePanel.current?.getTileForEventId(ev.getId());
if (tile) {
tile.forceUpdate();
}
};

private onVisibilityPowerLevelChange = (ev: MatrixEvent, member: RoomMember): void => {
if (this.unmounted) return;

// ignore events for other rooms
if (member.roomId !== this.props.timelineSet.room?.roomId) return;

// ignore events for other users
if (member.userId != MatrixClientPeg.get().credentials?.userId) return;

// We could skip an update if the power level change didn't cross the
// threshold for `VISIBILITY_CHANGE_TYPE`.
for (const event of this.state.events) {
const tile = this.messagePanel.current?.getTileForEventId(event.getId());
if (!tile) {
// The event is not visible, nothing to re-render.
continue;
}
tile.forceUpdate();
}

this.forceUpdate();
};

private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => {
if (this.unmounted) return;

Expand Down
55 changes: 55 additions & 0 deletions src/components/views/messages/HiddenBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";

import { _t } from "../../../languageHandler";
import { IBodyProps } from "./IBodyProps";

interface IProps {
mxEvent: MatrixEvent;
}

/**
* A message hidden from the user pending moderation.
*
* Note: This component must not be used when the user is the author of the message
* or has a sufficient powerlevel to see the message.
*/
const HiddenBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
let text;
const visibility = mxEvent.messageVisibility();
switch (visibility.visible) {
case true:
throw new Error("HiddenBody should only be applied to hidden messages");
case false:
if (visibility.reason) {
text = _t("Message pending moderation: %(reason)s", { reason: visibility.reason });
} else {
text = _t("Message pending moderation");
}
break;
}

return (
<span className="mx_HiddenBody" ref={ref}>
{ text }
</span>
);
});

export default HiddenBody;
8 changes: 8 additions & 0 deletions src/components/views/messages/IBodyProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export interface IBodyProps {
permalinkCreator: RoomPermalinkCreator;
mediaEventHelper: MediaEventHelper;

/*
If present and `true`, the message has been marked as hidden pending moderation
(see MSC3531) **but** the current user can see the message nevertheless (with
a marker), either because they are a moderator or because they are the original
author of the message.
*/
isSeeingThroughMessageHiddenForModeration?: boolean;

// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
}
9 changes: 8 additions & 1 deletion src/components/views/messages/MessageEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IOperableEventTile } from "../context_menus/MessageContextMenu";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { ReactAnyComponent } from "../../../@types/common";
import { IBodyProps } from "./IBodyProps";
import MatrixClientContext from '../../../contexts/MatrixClientContext';

// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
Expand All @@ -40,14 +41,19 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {

// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;

isSeeingThroughMessageHiddenForModeration?: boolean;
}

@replaceableComponent("views.messages.MessageEvent")
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private mediaHelper: MediaEventHelper;

public constructor(props: IProps) {
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;

public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props);

if (MediaEventHelper.isEligible(this.props.mxEvent)) {
Expand Down Expand Up @@ -171,6 +177,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
permalinkCreator={this.props.permalinkCreator}
mediaEventHelper={this.mediaHelper}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={this.props.isSeeingThroughMessageHiddenForModeration}
/> : null;
}
}
Loading