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 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):
|
||||
|
|
|
@ -183,6 +183,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";
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -909,12 +917,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;
|
||||
|
|
|
@ -45,7 +45,9 @@ limitations under the License.
|
|||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_RedactedBody {
|
||||
.mx_RedactedBody,
|
||||
.mx_HiddenBody {
|
||||
|
||||
padding: 4px 0 2px 20px;
|
||||
|
||||
&::before {
|
||||
|
|
|
@ -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>();
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
|
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;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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"> {
|
||||
|
@ -40,6 +41,8 @@ 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")
|
||||
|
@ -47,7 +50,10 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -297,7 +297,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||
nextProps.editState !== this.props.editState ||
|
||||
nextState.links !== this.state.links ||
|
||||
nextState.widgetHidden !== this.state.widgetHidden);
|
||||
nextState.widgetHidden !== this.state.widgetHidden ||
|
||||
nextProps.isSeeingThroughMessageHiddenForModeration
|
||||
!== this.props.isSeeingThroughMessageHiddenForModeration);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this.props.editState) {
|
||||
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() }
|
||||
</>;
|
||||
}
|
||||
if (this.props.isSeeingThroughMessageHiddenForModeration) {
|
||||
body = <>
|
||||
{ body }
|
||||
{ this.renderPendingModerationMarker() }
|
||||
</>;
|
||||
}
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||
|
|
|
@ -333,6 +333,12 @@ interface IProps {
|
|||
showThreadInfo?: boolean;
|
||||
|
||||
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 {
|
||||
|
@ -1038,7 +1044,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
||||
this.setState({ actionBarFocused });
|
||||
};
|
||||
|
||||
// TODO: Types
|
||||
private getTile: () => any | null = () => this.tile.current;
|
||||
|
||||
|
@ -1074,13 +1079,15 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
render() {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const eventType = this.props.mxEvent.getType() as EventType;
|
||||
const eventDisplayInfo = getEventDisplayInfo(this.props.mxEvent);
|
||||
const {
|
||||
tileHandler,
|
||||
isBubbleMessage,
|
||||
isInfoMessage,
|
||||
isLeftAlignedBubbleMessage,
|
||||
noBubbleEvent,
|
||||
} = getEventDisplayInfo(this.props.mxEvent);
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
} = eventDisplayInfo;
|
||||
const { isQuoteExpanded } = this.state;
|
||||
|
||||
// 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}
|
||||
editState={this.props.editState}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
|
@ -1413,6 +1421,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
editState={this.props.editState}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||
/>
|
||||
{ actionBar }
|
||||
{ timestamp }
|
||||
|
@ -1486,6 +1495,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
editState={this.props.editState}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
|
@ -1538,6 +1548,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
callEventGrouper={this.props.callEventGrouper}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||
/>
|
||||
{ keyRequestInfo }
|
||||
{ actionBar }
|
||||
|
|
|
@ -109,7 +109,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
const msgType = mxEvent.getContent().msgtype;
|
||||
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
|
||||
// before trying to instantiate us
|
||||
if (!tileHandler) {
|
||||
|
@ -174,7 +174,9 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
overrideEventTypes={evOverrides}
|
||||
replacingEventId={mxEvent.replacingEventId()}
|
||||
maxImageHeight={96}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent} />
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -863,6 +863,7 @@
|
|||
"Encryption": "Encryption",
|
||||
"Experimental": "Experimental",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -2085,6 +2086,8 @@
|
|||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||
"Encryption not enabled": "Encryption not enabled",
|
||||
"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",
|
||||
"React": "React",
|
||||
"Edit": "Edit",
|
||||
|
|
|
@ -175,6 +175,13 @@ export interface IFeature extends Omit<IBaseSetting, "isFeature"> {
|
|||
export type ISetting = IBaseSetting | IFeature;
|
||||
|
||||
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": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Moderation,
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { logger } from 'matrix-js-sdk/src/logger';
|
||||
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): {
|
||||
isInfoMessage: boolean;
|
||||
tileHandler: string;
|
||||
isBubbleMessage: boolean;
|
||||
isLeftAlignedBubbleMessage: boolean;
|
||||
noBubbleEvent: boolean;
|
||||
isSeeingThroughMessageHiddenForModeration: boolean;
|
||||
} {
|
||||
const content = mxEvent.getContent();
|
||||
const msgtype = content.msgtype;
|
||||
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
|
||||
let isBubbleMessage = (
|
||||
|
@ -168,7 +251,14 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
|
|||
isInfoMessage = true;
|
||||
}
|
||||
|
||||
return { tileHandler, isInfoMessage, isBubbleMessage, isLeftAlignedBubbleMessage, noBubbleEvent };
|
||||
return {
|
||||
tileHandler,
|
||||
isInfoMessage,
|
||||
isBubbleMessage,
|
||||
isLeftAlignedBubbleMessage,
|
||||
noBubbleEvent,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
};
|
||||
}
|
||||
|
||||
export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
|
||||
|
|
|
@ -124,7 +124,9 @@ a.mx_reply_anchor:hover {
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_RedactedBody {
|
||||
.mx_RedactedBody,
|
||||
.mx_HiddenBody {
|
||||
|
||||
padding-left: unset;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue