You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
MSC3531: Hiding messages during moderation (#2041)
This commit is contained in:
@@ -177,6 +177,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
|||||||
"io.element.functional_members",
|
"io.element.functional_members",
|
||||||
"io.element.functional_members");
|
"io.element.functional_members");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of message that affects visibility of a message,
|
||||||
|
* as per https://github.com/matrix-org/matrix-doc/pull/3531
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||||
|
"m.visibility",
|
||||||
|
"org.matrix.msc3531.visibility");
|
||||||
|
|
||||||
export interface IEncryptedFile {
|
export interface IEncryptedFile {
|
||||||
url: string;
|
url: string;
|
||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
|
@@ -41,7 +41,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
MsgType,
|
MsgType,
|
||||||
RelationType,
|
RelationType,
|
||||||
|
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||||
} from "../@types/event";
|
} from "../@types/event";
|
||||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||||
import { deepSortedObjectEntries } from "../utils";
|
import { deepSortedObjectEntries } from "../utils";
|
||||||
@@ -125,6 +126,35 @@ export interface IEventRelation {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVisibilityEventRelation extends IEventRelation {
|
||||||
|
visibility: "visible" | "hidden";
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an event is a visibility change event, as per MSC3531,
|
||||||
|
* the visibility change implied by the event.
|
||||||
|
*/
|
||||||
|
export interface IVisibilityChange {
|
||||||
|
/**
|
||||||
|
* If `true`, the target event should be made visible.
|
||||||
|
* Otherwise, it should be hidden.
|
||||||
|
*/
|
||||||
|
visible: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event id affected.
|
||||||
|
*/
|
||||||
|
eventId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally, a human-readable reason explaining why
|
||||||
|
* the event was hidden. Ignored if the event was made
|
||||||
|
* visible.
|
||||||
|
*/
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IClearEvent {
|
export interface IClearEvent {
|
||||||
room_id?: string;
|
room_id?: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -143,6 +173,30 @@ export interface IDecryptOptions {
|
|||||||
isRetry?: boolean;
|
isRetry?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||||
|
*/
|
||||||
|
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
|
||||||
|
/**
|
||||||
|
* Variant of `MessageVisibility` for the case in which the message should be displayed.
|
||||||
|
*/
|
||||||
|
export interface IMessageVisibilityVisible {
|
||||||
|
readonly visible: true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Variant of `MessageVisibility` for the case in which the message should be hidden.
|
||||||
|
*/
|
||||||
|
export interface IMessageVisibilityHidden {
|
||||||
|
readonly visible: false;
|
||||||
|
/**
|
||||||
|
* Optionally, a human-readable reason to show to the user indicating why the
|
||||||
|
* message has been hidden (e.g. "Message Pending Moderation").
|
||||||
|
*/
|
||||||
|
readonly reason: string | null;
|
||||||
|
}
|
||||||
|
// A singleton implementing `IMessageVisibilityVisible`.
|
||||||
|
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
|
||||||
|
|
||||||
export class MatrixEvent extends EventEmitter {
|
export class MatrixEvent extends EventEmitter {
|
||||||
private pushActions: IActionsObject = null;
|
private pushActions: IActionsObject = null;
|
||||||
private _replacingEvent: MatrixEvent = null;
|
private _replacingEvent: MatrixEvent = null;
|
||||||
@@ -150,6 +204,12 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
private _isCancelled = false;
|
private _isCancelled = false;
|
||||||
private clearEvent?: IClearEvent;
|
private clearEvent?: IClearEvent;
|
||||||
|
|
||||||
|
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||||
|
|
||||||
|
Note: We're returning this object, so any value stored here MUST be frozen.
|
||||||
|
*/
|
||||||
|
private visibility: MessageVisibility = MESSAGE_VISIBLE;
|
||||||
|
|
||||||
/* curve25519 key which we believe belongs to the sender of the event. See
|
/* curve25519 key which we believe belongs to the sender of the event. See
|
||||||
* getSenderKey()
|
* getSenderKey()
|
||||||
*/
|
*/
|
||||||
@@ -923,6 +983,53 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
|
||||||
|
*
|
||||||
|
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
|
||||||
|
* caused a change in the actual visibility of this event, either by making it
|
||||||
|
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
||||||
|
* changing the reason (if it was hidden).
|
||||||
|
* @param visibilityEvent event holding a hide/unhide payload, or nothing
|
||||||
|
* if the event is being reset to its original visibility (presumably
|
||||||
|
* by a visibility event being redacted).
|
||||||
|
*/
|
||||||
|
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
|
||||||
|
const visible = visibilityChange ? visibilityChange.visible : true;
|
||||||
|
const reason = visibilityChange ? visibilityChange.reason : null;
|
||||||
|
let change = false;
|
||||||
|
if (this.visibility.visible !== visibilityChange.visible) {
|
||||||
|
change = true;
|
||||||
|
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
if (change) {
|
||||||
|
if (visible) {
|
||||||
|
this.visibility = MESSAGE_VISIBLE;
|
||||||
|
} else {
|
||||||
|
this.visibility = Object.freeze({
|
||||||
|
visible: false,
|
||||||
|
reason: reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (change) {
|
||||||
|
this.emit("Event.visibilityChange", this, visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return instructions to display or hide the message.
|
||||||
|
*
|
||||||
|
* @returns Instructions determining whether the message
|
||||||
|
* should be displayed.
|
||||||
|
*/
|
||||||
|
public messageVisibility(): MessageVisibility {
|
||||||
|
// Note: We may return `this.visibility` without fear, as
|
||||||
|
// this is a shallow frozen object.
|
||||||
|
return this.visibility;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the content of an event in the same way it would be by the server
|
* Update the content of an event in the same way it would be by the server
|
||||||
* if it were redacted before it was sent to us
|
* if it were redacted before it was sent to us
|
||||||
@@ -992,6 +1099,54 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
return this.getType() === EventType.RoomRedaction;
|
return this.getType() === EventType.RoomRedaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the visibility change caused by this event,
|
||||||
|
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||||
|
*
|
||||||
|
* @returns If the event is a well-formed visibility change event,
|
||||||
|
* an instance of `IVisibilityChange`, otherwise `null`.
|
||||||
|
*/
|
||||||
|
public asVisibilityChange(): IVisibilityChange | null {
|
||||||
|
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
|
||||||
|
// Not a visibility change event.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const relation = this.getRelation();
|
||||||
|
if (!relation || relation.rel_type != "m.reference") {
|
||||||
|
// Ill-formed, ignore this event.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const eventId = relation.event_id;
|
||||||
|
if (!eventId) {
|
||||||
|
// Ill-formed, ignore this event.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const content = this.getWireContent();
|
||||||
|
const visible = !!content.visible;
|
||||||
|
const reason = content.reason;
|
||||||
|
if (reason && typeof reason != "string") {
|
||||||
|
// Ill-formed, ignore this event.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Well-formed visibility change event.
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
reason,
|
||||||
|
eventId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this event alters the visibility of another event,
|
||||||
|
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if this event alters the visibility
|
||||||
|
* of another event.
|
||||||
|
*/
|
||||||
|
public isVisibilityEvent(): boolean {
|
||||||
|
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the (decrypted, if necessary) redaction event JSON
|
* Get the (decrypted, if necessary) redaction event JSON
|
||||||
* if event was redacted
|
* if event was redacted
|
||||||
|
@@ -618,14 +618,14 @@ export class RoomState extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given MatrixClient has permission to send a state
|
* Returns true if the given MatrixClient has permission to send a state
|
||||||
* event of type `stateEventType` into this room.
|
* event of type `stateEventType` into this room.
|
||||||
* @param {string} stateEventType The type of state events to test
|
* @param {string} stateEventType The type of state events to test
|
||||||
* @param {MatrixClient} cli The client to test permission for
|
* @param {MatrixClient} cli The client to test permission for
|
||||||
* @return {boolean} true if the given client should be permitted to send
|
* @return {boolean} true if the given client should be permitted to send
|
||||||
* the given type of state event into this room,
|
* the given type of state event into this room,
|
||||||
* according to the room's state.
|
* according to the room's state.
|
||||||
*/
|
*/
|
||||||
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
|
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
|
||||||
if (cli.isGuest()) {
|
if (cli.isGuest()) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -30,7 +30,10 @@ import { RoomMember } from "./room-member";
|
|||||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ReEmitter } from '../ReEmitter';
|
import { ReEmitter } from '../ReEmitter';
|
||||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
import {
|
||||||
|
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||||
|
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||||
|
} from "../@types/event";
|
||||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
@@ -105,6 +108,22 @@ interface IReceiptContent {
|
|||||||
|
|
||||||
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
|
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
|
||||||
|
|
||||||
|
// When inserting a visibility event affecting event `eventId`, we
|
||||||
|
// need to scan through existing visibility events for `eventId`.
|
||||||
|
// In theory, this could take an unlimited amount of time if:
|
||||||
|
//
|
||||||
|
// - the visibility event was sent by a moderator; and
|
||||||
|
// - `eventId` already has many visibility changes (usually, it should
|
||||||
|
// be 2 or less); and
|
||||||
|
// - for some reason, the visibility changes are received out of order
|
||||||
|
// (usually, this shouldn't happen at all).
|
||||||
|
//
|
||||||
|
// For this reason, we limit the number of events to scan through,
|
||||||
|
// expecting that a broken visibility change for a single event in
|
||||||
|
// an extremely uncommon case (possibly a DoS) is a small
|
||||||
|
// price to pay to keep matrix-js-sdk responsive.
|
||||||
|
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
|
||||||
|
|
||||||
export enum NotificationCountType {
|
export enum NotificationCountType {
|
||||||
Highlight = "highlight",
|
Highlight = "highlight",
|
||||||
Total = "total",
|
Total = "total",
|
||||||
@@ -193,6 +212,24 @@ export class Room extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public threads = new Map<string, Thread>();
|
public threads = new Map<string, Thread>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of eventId to all visibility changes to apply
|
||||||
|
* to the event, by chronological order, as per
|
||||||
|
* https://github.com/matrix-org/matrix-doc/pull/3531
|
||||||
|
*
|
||||||
|
* # Invariants
|
||||||
|
*
|
||||||
|
* - within each list, all events are classed by
|
||||||
|
* chronological order;
|
||||||
|
* - all events are events such that
|
||||||
|
* `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
|
||||||
|
* - within each list with key `eventId`, all events
|
||||||
|
* are in relation to `eventId`.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
private visibilityEvents = new Map<string, MatrixEvent[]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new Room.
|
* Construct a new Room.
|
||||||
*
|
*
|
||||||
@@ -253,7 +290,9 @@ export class Room extends EventEmitter {
|
|||||||
// all our per-room timeline sets. the first one is the unfiltered ones;
|
// all our per-room timeline sets. the first one is the unfiltered ones;
|
||||||
// the subsequent ones are the filtered ones in no particular order.
|
// the subsequent ones are the filtered ones in no particular order.
|
||||||
this.timelineSets = [new EventTimelineSet(this, opts)];
|
this.timelineSets = [new EventTimelineSet(this, opts)];
|
||||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
|
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
|
||||||
|
"Room.timeline", "Room.timelineReset",
|
||||||
|
]);
|
||||||
|
|
||||||
this.fixUpLegacyTimelineFields();
|
this.fixUpLegacyTimelineFields();
|
||||||
|
|
||||||
@@ -1409,8 +1448,26 @@ export class Room extends EventEmitter {
|
|||||||
// NB: We continue to add the redaction event to the timeline so
|
// NB: We continue to add the redaction event to the timeline so
|
||||||
// clients can say "so and so redacted an event" if they wish to. Also
|
// clients can say "so and so redacted an event" if they wish to. Also
|
||||||
// this may be needed to trigger an update.
|
// this may be needed to trigger an update.
|
||||||
|
|
||||||
|
// Remove any visibility change on this event.
|
||||||
|
this.visibilityEvents.delete(redactId);
|
||||||
|
|
||||||
|
// If this event is a visibility change event, remove it from the
|
||||||
|
// list of visibility changes and update any event affected by it.
|
||||||
|
if (redactedEvent.isVisibilityEvent()) {
|
||||||
|
this.redactVisibilityChangeEvent(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement MSC3531: hiding messages.
|
||||||
|
if (event.isVisibilityEvent()) {
|
||||||
|
// This event changes the visibility of another event, record
|
||||||
|
// the visibility change, inform clients if necessary.
|
||||||
|
this.applyNewVisibilityEvent(event);
|
||||||
|
}
|
||||||
|
// If any pending visibility change is waiting for this (older) event,
|
||||||
|
this.applyPendingVisibilityEvents(event);
|
||||||
|
|
||||||
if (event.getUnsigned().transaction_id) {
|
if (event.getUnsigned().transaction_id) {
|
||||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||||
if (existingEvent) {
|
if (existingEvent) {
|
||||||
@@ -2292,6 +2349,161 @@ export class Room extends EventEmitter {
|
|||||||
return "Empty room";
|
return "Empty room";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we receive a new visibility change event:
|
||||||
|
*
|
||||||
|
* - store this visibility change alongside the timeline, in case we
|
||||||
|
* later need to apply it to an event that we haven't received yet;
|
||||||
|
* - if we have already received the event whose visibility has changed,
|
||||||
|
* patch it to reflect the visibility change and inform listeners.
|
||||||
|
*/
|
||||||
|
private applyNewVisibilityEvent(event: MatrixEvent): void {
|
||||||
|
const visibilityChange = event.asVisibilityChange();
|
||||||
|
if (!visibilityChange) {
|
||||||
|
// The event is ill-formed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore visibility change events that are not emitted by moderators.
|
||||||
|
const userId = event.getSender();
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isPowerSufficient =
|
||||||
|
(
|
||||||
|
EVENT_VISIBILITY_CHANGE_TYPE.name
|
||||||
|
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)
|
||||||
|
)
|
||||||
|
|| (
|
||||||
|
EVENT_VISIBILITY_CHANGE_TYPE.altName
|
||||||
|
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)
|
||||||
|
);
|
||||||
|
if (!isPowerSufficient) {
|
||||||
|
// Powerlevel is insufficient.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record this change in visibility.
|
||||||
|
// If the event is not in our timeline and we only receive it later,
|
||||||
|
// we may need to apply the visibility change at a later date.
|
||||||
|
|
||||||
|
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
|
||||||
|
if (visibilityEventsOnOriginalEvent) {
|
||||||
|
// It would be tempting to simply erase the latest visibility change
|
||||||
|
// but we need to record all of the changes in case the latest change
|
||||||
|
// is ever redacted.
|
||||||
|
//
|
||||||
|
// In practice, linear scans through `visibilityEvents` should be fast.
|
||||||
|
// However, to protect against a potential DoS attack, we limit the
|
||||||
|
// number of iterations in this loop.
|
||||||
|
let index = visibilityEventsOnOriginalEvent.length - 1;
|
||||||
|
const min = Math.max(0,
|
||||||
|
visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
|
||||||
|
for (; index >= min; --index) {
|
||||||
|
const target = visibilityEventsOnOriginalEvent[index];
|
||||||
|
if (target.getTs() < event.getTs()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index === -1) {
|
||||||
|
visibilityEventsOnOriginalEvent.unshift(event);
|
||||||
|
} else {
|
||||||
|
visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.visibilityEvents.set(visibilityChange.eventId, [event]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, let's check if the event is already in our timeline.
|
||||||
|
// If so, we need to patch it and inform listeners.
|
||||||
|
|
||||||
|
const originalEvent = this.findEventById(visibilityChange.eventId);
|
||||||
|
if (!originalEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originalEvent.applyVisibilityEvent(visibilityChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private redactVisibilityChangeEvent(event: MatrixEvent) {
|
||||||
|
// Sanity checks.
|
||||||
|
if (!event.isVisibilityEvent) {
|
||||||
|
throw new Error("expected a visibility change event");
|
||||||
|
}
|
||||||
|
const relation = event.getRelation();
|
||||||
|
const originalEventId = relation.event_id;
|
||||||
|
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
|
||||||
|
if (!visibilityEventsOnOriginalEvent) {
|
||||||
|
// No visibility changes on the original event.
|
||||||
|
// In particular, this change event was not recorded,
|
||||||
|
// most likely because it was ill-formed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
|
||||||
|
if (index === -1) {
|
||||||
|
// This change event was not recorded, most likely because
|
||||||
|
// it was ill-formed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove visibility change.
|
||||||
|
visibilityEventsOnOriginalEvent.splice(index, 1);
|
||||||
|
|
||||||
|
// If we removed the latest visibility change event, propagate changes.
|
||||||
|
if (index === visibilityEventsOnOriginalEvent.length) {
|
||||||
|
const originalEvent = this.findEventById(originalEventId);
|
||||||
|
if (!originalEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === 0) {
|
||||||
|
// We have just removed the only visibility change event.
|
||||||
|
this.visibilityEvents.delete(originalEventId);
|
||||||
|
originalEvent.applyVisibilityEvent();
|
||||||
|
} else {
|
||||||
|
const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
|
||||||
|
const newVisibility = newEvent.asVisibilityChange();
|
||||||
|
if (!newVisibility) {
|
||||||
|
// Event is ill-formed.
|
||||||
|
// This breaks our invariant.
|
||||||
|
throw new Error("at this stage, visibility changes should be well-formed");
|
||||||
|
}
|
||||||
|
originalEvent.applyVisibilityEvent(newVisibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we receive an event whose visibility has been altered by
|
||||||
|
* a (more recent) visibility change event, patch the event in
|
||||||
|
* place so that clients now not to display it.
|
||||||
|
*
|
||||||
|
* @param event Any matrix event. If this event has at least one a
|
||||||
|
* pending visibility change event, apply the latest visibility
|
||||||
|
* change event.
|
||||||
|
*/
|
||||||
|
private applyPendingVisibilityEvents(event: MatrixEvent): void {
|
||||||
|
const visibilityEvents = this.visibilityEvents.get(event.getId());
|
||||||
|
if (!visibilityEvents || visibilityEvents.length == 0) {
|
||||||
|
// No pending visibility change in store.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
|
||||||
|
const visibilityChange = visibilityEvent.asVisibilityChange();
|
||||||
|
if (!visibilityChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (visibilityChange.visible) {
|
||||||
|
// Events are visible by default, no need to apply a visibility change.
|
||||||
|
// Note that we need to keep the visibility changes in `visibilityEvents`,
|
||||||
|
// in case we later fetch an older visibility change event that is superseded
|
||||||
|
// by `visibilityChange`.
|
||||||
|
}
|
||||||
|
if (visibilityEvent.getTs() < event.getTs()) {
|
||||||
|
// Something is wrong, the visibility change cannot happen before the
|
||||||
|
// event. Presumably an ill-formed event.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.applyVisibilityEvent(visibilityChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2460,3 +2672,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
|
|||||||
* @param {string} membership The new membership value
|
* @param {string} membership The new membership value
|
||||||
* @param {string} prevMembership The previous membership value
|
* @param {string} prevMembership The previous membership value
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@@ -201,6 +201,7 @@ export class SyncApi {
|
|||||||
"Room.accountData",
|
"Room.accountData",
|
||||||
"Room.myMembership",
|
"Room.myMembership",
|
||||||
"Room.replaceEvent",
|
"Room.replaceEvent",
|
||||||
|
"Room.visibilityChange",
|
||||||
]);
|
]);
|
||||||
this.registerStateListeners(room);
|
this.registerStateListeners(room);
|
||||||
return room;
|
return room;
|
||||||
|
Reference in New Issue
Block a user