MSC3531 - Implementing message hiding pending moderation (#7518)
Signed-off-by: David Teller <davidt@element.io>
This commit is contained in:
parent
c612014936
commit
6b870ba1a9
17 changed files with 345 additions and 22 deletions
|
@ -25,7 +25,7 @@ that room administrators cannot force account-only settings upon participants.
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Settings are the different options a user may set or experience in the application. These are pre-defined in
|
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 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):
|
settings, like the "theme" setting, are special cased in the config file):
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
@import "./views/messages/_EventTileBubble.scss";
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
|
@import "./views/messages/_HiddenBody.scss";
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
|
37
res/css/views/messages/_HiddenBody.scss
Normal file
37
res/css/views/messages/_HiddenBody.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -396,6 +396,14 @@ $left-gutter: 64px;
|
||||||
cursor: pointer;
|
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 {
|
.mx_EventTile_e2eIcon {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
@ -909,12 +917,14 @@ $left-gutter: 64px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.mx_EventTile_content,
|
.mx_EventTile_content,
|
||||||
|
.mx_HiddenBody,
|
||||||
.mx_RedactedBody,
|
.mx_RedactedBody,
|
||||||
.mx_ReplyChain_wrapper {
|
.mx_ReplyChain_wrapper {
|
||||||
margin-left: 36px;
|
margin-left: 36px;
|
||||||
margin-right: 50px;
|
margin-right: 50px;
|
||||||
|
|
||||||
.mx_EventTile_content,
|
.mx_EventTile_content,
|
||||||
|
.mx_HiddenBody,
|
||||||
.mx_RedactedBody,
|
.mx_RedactedBody,
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -45,7 +45,9 @@ limitations under the License.
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RedactedBody {
|
.mx_RedactedBody,
|
||||||
|
.mx_HiddenBody {
|
||||||
|
|
||||||
padding: 4px 0 2px 20px;
|
padding: 4px 0 2px 20px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -250,7 +250,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
private scrollPanel = createRef<ScrollPanel>();
|
private scrollPanel = createRef<ScrollPanel>();
|
||||||
|
|
||||||
private readonly showTypingNotificationsWatcherRef: string;
|
private readonly showTypingNotificationsWatcherRef: string;
|
||||||
private eventNodes: Record<string, HTMLElement>;
|
private eventTiles: Record<string, EventTile> = {};
|
||||||
|
|
||||||
// A map of <callId, CallEventGrouper>
|
// A map of <callId, CallEventGrouper>
|
||||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||||
|
@ -324,11 +324,18 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
/* get the DOM node representing the given event */
|
/* get the DOM node representing the given event */
|
||||||
public getNodeForEventId(eventId: string): HTMLElement {
|
public getNodeForEventId(eventId: string): HTMLElement {
|
||||||
if (!this.eventNodes) {
|
if (!this.eventTiles) {
|
||||||
return undefined;
|
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.
|
/* return true if the content is fully scrolled down right now; else false.
|
||||||
|
@ -429,7 +436,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollToEventIfNeeded(eventId: string): void {
|
public scrollToEventIfNeeded(eventId: string): void {
|
||||||
const node = this.eventNodes[eventId];
|
const node = this.getNodeForEventId(eventId);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.scrollIntoView({
|
node.scrollIntoView({
|
||||||
block: "nearest",
|
block: "nearest",
|
||||||
|
@ -584,8 +591,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private getEventTiles(): ReactNode[] {
|
private getEventTiles(): ReactNode[] {
|
||||||
this.eventNodes = {};
|
|
||||||
|
|
||||||
let i;
|
let i;
|
||||||
|
|
||||||
// first figure out which is the last event in the list which we're
|
// first figure out which is the last event in the list which we're
|
||||||
|
@ -776,7 +781,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
<EventTile
|
<EventTile
|
||||||
as="li"
|
as="li"
|
||||||
ref={this.collectEventNode.bind(this, eventId)}
|
ref={this.collectEventTile.bind(this, eventId)}
|
||||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
mxEvent={mxEv}
|
mxEvent={mxEv}
|
||||||
continuation={continuation}
|
continuation={continuation}
|
||||||
|
@ -909,8 +914,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
return receiptsByEvent;
|
return receiptsByEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectEventNode = (eventId: string, node: EventTile): void => {
|
private collectEventTile = (eventId: string, node: EventTile): void => {
|
||||||
this.eventNodes[eventId] = node?.ref?.current;
|
this.eventTiles[eventId] = node;
|
||||||
};
|
};
|
||||||
|
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -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 { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
|
||||||
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
|
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import { SyncState } from 'matrix-js-sdk/src/sync';
|
import { SyncState } from 'matrix-js-sdk/src/sync';
|
||||||
|
import { RoomMember } from 'matrix-js-sdk';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
@ -276,6 +277,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
cli.on("Room.timeline", this.onRoomTimeline);
|
cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
cli.on("Room.timelineReset", this.onRoomTimelineReset);
|
cli.on("Room.timelineReset", this.onRoomTimelineReset);
|
||||||
cli.on("Room.redaction", this.onRoomRedaction);
|
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
|
// same event handler as Room.redaction as for both we just do forceUpdate
|
||||||
cli.on("Room.redactionCancelled", this.onRoomRedaction);
|
cli.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
|
@ -352,8 +358,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
client.removeListener("Room.accountData", this.onAccountData);
|
client.removeListener("Room.accountData", this.onAccountData);
|
||||||
|
client.removeListener("RoomMember.powerLevel", this.onVisibilityPowerLevelChange);
|
||||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
client.removeListener("Event.replaced", this.onEventReplaced);
|
client.removeListener("Event.replaced", this.onEventReplaced);
|
||||||
|
client.removeListener("Event.visibilityChange", this.onEventVisibilityChange);
|
||||||
client.removeListener("sync", this.onSync);
|
client.removeListener("sync", this.onSync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -619,6 +627,50 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
this.forceUpdate();
|
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 => {
|
private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
|
55
src/components/views/messages/HiddenBody.tsx
Normal file
55
src/components/views/messages/HiddenBody.tsx
Normal file
|
@ -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;
|
|
@ -44,6 +44,14 @@ export interface IBodyProps {
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
mediaEventHelper: MediaEventHelper;
|
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
|
// helper function to access relations for this event
|
||||||
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { IOperableEventTile } from "../context_menus/MessageContextMenu";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import { ReactAnyComponent } from "../../../@types/common";
|
import { ReactAnyComponent } from "../../../@types/common";
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
|
|
||||||
// onMessageAllowed is handled internally
|
// onMessageAllowed is handled internally
|
||||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
|
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
|
||||||
|
@ -40,6 +41,8 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
|
||||||
|
|
||||||
// helper function to access relations for this event
|
// helper function to access relations for this event
|
||||||
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
|
|
||||||
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MessageEvent")
|
@replaceableComponent("views.messages.MessageEvent")
|
||||||
|
@ -47,7 +50,10 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||||
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
||||||
private mediaHelper: MediaEventHelper;
|
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);
|
super(props);
|
||||||
|
|
||||||
if (MediaEventHelper.isEligible(this.props.mxEvent)) {
|
if (MediaEventHelper.isEligible(this.props.mxEvent)) {
|
||||||
|
@ -171,6 +177,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
mediaEventHelper={this.mediaHelper}
|
mediaEventHelper={this.mediaHelper}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={this.props.isSeeingThroughMessageHiddenForModeration}
|
||||||
/> : null;
|
/> : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,7 +297,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||||
nextProps.editState !== this.props.editState ||
|
nextProps.editState !== this.props.editState ||
|
||||||
nextState.links !== this.state.links ||
|
nextState.links !== this.state.links ||
|
||||||
nextState.widgetHidden !== this.state.widgetHidden);
|
nextState.widgetHidden !== this.state.widgetHidden ||
|
||||||
|
nextProps.isSeeingThroughMessageHiddenForModeration
|
||||||
|
!== this.props.isSeeingThroughMessageHiddenForModeration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateUrlPreview(): void {
|
private calculateUrlPreview(): void {
|
||||||
|
@ -504,6 +506,29 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a marker informing the user that, while they can see the message,
|
||||||
|
* it is hidden for other users.
|
||||||
|
*/
|
||||||
|
private renderPendingModerationMarker() {
|
||||||
|
let text;
|
||||||
|
const visibility = this.props.mxEvent.messageVisibility();
|
||||||
|
switch (visibility.visible) {
|
||||||
|
case true:
|
||||||
|
throw new Error("renderPendingModerationMarker 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_EventTile_pendingModeration">{ `(${text})` }</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.props.editState) {
|
if (this.props.editState) {
|
||||||
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
||||||
|
@ -554,6 +579,12 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
{ this.renderEditedMarker() }
|
{ this.renderEditedMarker() }
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
if (this.props.isSeeingThroughMessageHiddenForModeration) {
|
||||||
|
body = <>
|
||||||
|
{ body }
|
||||||
|
{ this.renderPendingModerationMarker() }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.highlightLink) {
|
if (this.props.highlightLink) {
|
||||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||||
|
|
|
@ -333,6 +333,12 @@ interface IProps {
|
||||||
showThreadInfo?: boolean;
|
showThreadInfo?: boolean;
|
||||||
|
|
||||||
timelineRenderingType?: TimelineRenderingType;
|
timelineRenderingType?: TimelineRenderingType;
|
||||||
|
|
||||||
|
// if specified and `true`, the message his behing
|
||||||
|
// hidden for moderation from other users but is
|
||||||
|
// displayed to the current user either because they're
|
||||||
|
// the author or they are a moderator
|
||||||
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -1038,7 +1044,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
||||||
this.setState({ actionBarFocused });
|
this.setState({ actionBarFocused });
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Types
|
// TODO: Types
|
||||||
private getTile: () => any | null = () => this.tile.current;
|
private getTile: () => any | null = () => this.tile.current;
|
||||||
|
|
||||||
|
@ -1074,13 +1079,15 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
render() {
|
render() {
|
||||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||||
const eventType = this.props.mxEvent.getType() as EventType;
|
const eventType = this.props.mxEvent.getType() as EventType;
|
||||||
|
const eventDisplayInfo = getEventDisplayInfo(this.props.mxEvent);
|
||||||
const {
|
const {
|
||||||
tileHandler,
|
tileHandler,
|
||||||
isBubbleMessage,
|
isBubbleMessage,
|
||||||
isInfoMessage,
|
isInfoMessage,
|
||||||
isLeftAlignedBubbleMessage,
|
isLeftAlignedBubbleMessage,
|
||||||
noBubbleEvent,
|
noBubbleEvent,
|
||||||
} = getEventDisplayInfo(this.props.mxEvent);
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
} = eventDisplayInfo;
|
||||||
const { isQuoteExpanded } = this.state;
|
const { isQuoteExpanded } = this.state;
|
||||||
|
|
||||||
// This shouldn't happen: the caller should check we support this type
|
// This shouldn't happen: the caller should check we support this type
|
||||||
|
@ -1371,6 +1378,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
@ -1413,6 +1421,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||||
/>
|
/>
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
|
@ -1486,6 +1495,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<a
|
<a
|
||||||
|
@ -1538,6 +1548,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
callEventGrouper={this.props.callEventGrouper}
|
callEventGrouper={this.props.callEventGrouper}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||||
/>
|
/>
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
const msgType = mxEvent.getContent().msgtype;
|
const msgType = mxEvent.getContent().msgtype;
|
||||||
const evType = mxEvent.getType() as EventType;
|
const evType = mxEvent.getType() as EventType;
|
||||||
|
|
||||||
const { tileHandler, isInfoMessage } = getEventDisplayInfo(mxEvent);
|
const { tileHandler, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(mxEvent);
|
||||||
// This shouldn't happen: the caller should check we support this type
|
// This shouldn't happen: the caller should check we support this type
|
||||||
// before trying to instantiate us
|
// before trying to instantiate us
|
||||||
if (!tileHandler) {
|
if (!tileHandler) {
|
||||||
|
@ -174,7 +174,9 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
overrideEventTypes={evOverrides}
|
overrideEventTypes={evOverrides}
|
||||||
replacingEventId={mxEvent.replacingEventId()}
|
replacingEventId={mxEvent.replacingEventId()}
|
||||||
maxImageHeight={96}
|
maxImageHeight={96}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent} />
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -863,6 +863,7 @@
|
||||||
"Encryption": "Encryption",
|
"Encryption": "Encryption",
|
||||||
"Experimental": "Experimental",
|
"Experimental": "Experimental",
|
||||||
"Developer": "Developer",
|
"Developer": "Developer",
|
||||||
|
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
||||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
||||||
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
||||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||||
|
@ -2085,6 +2086,8 @@
|
||||||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||||
"Encryption not enabled": "Encryption not enabled",
|
"Encryption not enabled": "Encryption not enabled",
|
||||||
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
|
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
|
||||||
|
"Message pending moderation: %(reason)s": "Message pending moderation: %(reason)s",
|
||||||
|
"Message pending moderation": "Message pending moderation",
|
||||||
"Error processing audio message": "Error processing audio message",
|
"Error processing audio message": "Error processing audio message",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
|
|
@ -175,6 +175,13 @@ export interface IFeature extends Omit<IBaseSetting, "isFeature"> {
|
||||||
export type ISetting = IBaseSetting | IFeature;
|
export type ISetting = IBaseSetting | IFeature;
|
||||||
|
|
||||||
export const SETTINGS: {[setting: string]: ISetting} = {
|
export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
|
"feature_msc3531_hide_messages_pending_moderation": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Moderation,
|
||||||
|
displayName: _td("Let moderators hide messages pending moderation."),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_report_to_moderators": {
|
"feature_report_to_moderators": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Moderation,
|
labsGroup: LabGroup.Moderation,
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls";
|
import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls";
|
||||||
|
@ -114,18 +114,101 @@ export function findEditableEvent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How we should render a message depending on its moderation state.
|
||||||
|
*/
|
||||||
|
enum MessageModerationState {
|
||||||
|
/**
|
||||||
|
* The message is visible to all.
|
||||||
|
*/
|
||||||
|
VISIBLE_FOR_ALL = "VISIBLE_FOR_ALL",
|
||||||
|
/**
|
||||||
|
* The message is hidden pending moderation and we're not a user who should
|
||||||
|
* see it nevertheless.
|
||||||
|
*/
|
||||||
|
HIDDEN_TO_CURRENT_USER = "HIDDEN_TO_CURRENT_USER",
|
||||||
|
/**
|
||||||
|
* The message is hidden pending moderation and we're either the author of
|
||||||
|
* the message or a moderator. In either case, we need to see the message
|
||||||
|
* with a marker.
|
||||||
|
*/
|
||||||
|
SEE_THROUGH_FOR_CURRENT_USER = "SEE_THROUGH_FOR_CURRENT_USER",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a message should be displayed as hidden pending moderation.
|
||||||
|
*
|
||||||
|
* If MSC3531 is deactivated in settings, all messages are considered visible
|
||||||
|
* to all.
|
||||||
|
*/
|
||||||
|
export function getMessageModerationState(mxEvent: MatrixEvent): MessageModerationState {
|
||||||
|
if (!SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) {
|
||||||
|
return MessageModerationState.VISIBLE_FOR_ALL;
|
||||||
|
}
|
||||||
|
const visibility = mxEvent.messageVisibility();
|
||||||
|
if (visibility.visible) {
|
||||||
|
return MessageModerationState.VISIBLE_FOR_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we know that the message is marked as hidden
|
||||||
|
// pending moderation. However, if we're the author or a moderator,
|
||||||
|
// we still need to display it.
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (mxEvent.sender?.userId === client.getUserId()) {
|
||||||
|
// We're the author, show the message.
|
||||||
|
return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = client.getRoom(mxEvent.getRoomId());
|
||||||
|
if (EVENT_VISIBILITY_CHANGE_TYPE.name
|
||||||
|
&& room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, client.getUserId())
|
||||||
|
) {
|
||||||
|
// We're a moderator (as indicated by prefixed event name), show the message.
|
||||||
|
return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER;
|
||||||
|
}
|
||||||
|
if (EVENT_VISIBILITY_CHANGE_TYPE.altName
|
||||||
|
&& room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, client.getUserId())
|
||||||
|
) {
|
||||||
|
// We're a moderator (as indicated by unprefixed event name), show the message.
|
||||||
|
return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER;
|
||||||
|
}
|
||||||
|
// For everybody else, hide the message.
|
||||||
|
return MessageModerationState.HIDDEN_TO_CURRENT_USER;
|
||||||
|
}
|
||||||
|
|
||||||
export function getEventDisplayInfo(mxEvent: MatrixEvent): {
|
export function getEventDisplayInfo(mxEvent: MatrixEvent): {
|
||||||
isInfoMessage: boolean;
|
isInfoMessage: boolean;
|
||||||
tileHandler: string;
|
tileHandler: string;
|
||||||
isBubbleMessage: boolean;
|
isBubbleMessage: boolean;
|
||||||
isLeftAlignedBubbleMessage: boolean;
|
isLeftAlignedBubbleMessage: boolean;
|
||||||
noBubbleEvent: boolean;
|
noBubbleEvent: boolean;
|
||||||
|
isSeeingThroughMessageHiddenForModeration: boolean;
|
||||||
} {
|
} {
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
const msgtype = content.msgtype;
|
const msgtype = content.msgtype;
|
||||||
const eventType = mxEvent.getType();
|
const eventType = mxEvent.getType();
|
||||||
|
|
||||||
let tileHandler = getHandlerTile(mxEvent);
|
let isSeeingThroughMessageHiddenForModeration = false;
|
||||||
|
let tileHandler;
|
||||||
|
if (SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) {
|
||||||
|
switch (getMessageModerationState(mxEvent)) {
|
||||||
|
case MessageModerationState.VISIBLE_FOR_ALL:
|
||||||
|
// Default behavior, nothing to do.
|
||||||
|
break;
|
||||||
|
case MessageModerationState.HIDDEN_TO_CURRENT_USER:
|
||||||
|
// Hide message.
|
||||||
|
tileHandler = "messages.HiddenBody";
|
||||||
|
break;
|
||||||
|
case MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER:
|
||||||
|
// Show message with a marker.
|
||||||
|
isSeeingThroughMessageHiddenForModeration = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tileHandler) {
|
||||||
|
tileHandler = getHandlerTile(mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
// Info messages are basically information about commands processed on a room
|
// Info messages are basically information about commands processed on a room
|
||||||
let isBubbleMessage = (
|
let isBubbleMessage = (
|
||||||
|
@ -168,7 +251,14 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
|
||||||
isInfoMessage = true;
|
isInfoMessage = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tileHandler, isInfoMessage, isBubbleMessage, isLeftAlignedBubbleMessage, noBubbleEvent };
|
return {
|
||||||
|
tileHandler,
|
||||||
|
isInfoMessage,
|
||||||
|
isBubbleMessage,
|
||||||
|
isLeftAlignedBubbleMessage,
|
||||||
|
noBubbleEvent,
|
||||||
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
|
export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
|
||||||
|
|
|
@ -124,7 +124,9 @@ a.mx_reply_anchor:hover {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RedactedBody {
|
.mx_RedactedBody,
|
||||||
|
.mx_HiddenBody {
|
||||||
|
|
||||||
padding-left: unset;
|
padding-left: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue