Fix pin/unpin slowness and non refresh from the message action bar (#12934)

* Improve PinningUtils.ts doc and use common methods to check pin or unpin.
Removed unused methods.

* Send room account data and state event in parallel

* Rerender MessageActionBar.tsx if there is a room pinned event

* Update pinning util tests

* Add test for room pinned events in MessageActionBar-test.tsx
dbkr/sss
Florian Duros 2024-08-28 10:56:46 +02:00 committed by GitHub
parent 43941efbdb
commit ea3c5cf787
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 16 deletions

View File

@ -24,6 +24,9 @@ import {
MsgType, MsgType,
RelationType, RelationType,
M_BEACON_INFO, M_BEACON_INFO,
EventTimeline,
RoomStateEvent,
EventType,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import classNames from "classnames"; import classNames from "classnames";
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin.svg"; import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin.svg";
@ -278,12 +281,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
this.props.mxEvent.once(MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.once(MatrixEventEvent.Decrypted, this.onDecrypted);
} }
this.props.mxEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.props.mxEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.context.room
?.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.on(RoomStateEvent.Events, this.onRoomEvent);
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.props.mxEvent.off(MatrixEventEvent.Status, this.onSent); this.props.mxEvent.off(MatrixEventEvent.Status, this.onSent);
this.props.mxEvent.off(MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.off(MatrixEventEvent.Decrypted, this.onDecrypted);
this.props.mxEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.props.mxEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.context.room
?.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.off(RoomStateEvent.Events, this.onRoomEvent);
} }
private onDecrypted = (): void => { private onDecrypted = (): void => {
@ -297,6 +308,12 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
this.forceUpdate(); this.forceUpdate();
}; };
private onRoomEvent = (event?: MatrixEvent): void => {
// If the event is pinned or unpinned, rerender the component.
if (!event || event.getType() !== EventType.RoomPinnedEvents) return;
this.forceUpdate();
};
private onSent = (): void => { private onSent = (): void => {
// When an event is sent and echoed the possible actions change. // When an event is sent and echoed the possible actions change.
this.forceUpdate(); this.forceUpdate();

View File

@ -286,10 +286,6 @@ export function hasThreadSummary(event: MatrixEvent): boolean {
return event.isThreadRoot && !!event.getThread()?.length && !!event.getThread()!.replyToEvent; return event.isThreadRoot && !!event.getThread()?.length && !!event.getThread()!.replyToEvent;
} }
export function canPinEvent(event: MatrixEvent): boolean {
return !M_BEACON_INFO.matches(event.getType());
}
export const highlightEvent = (roomId: string, eventId: string): void => { export const highlightEvent = (roomId: string, eventId: string): void => {
defaultDispatcher.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,

View File

@ -16,7 +16,7 @@ limitations under the License.
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix";
import { canPinEvent, isContentActionable } from "./EventUtils"; import { isContentActionable } from "./EventUtils";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { ReadPinsEventId } from "../components/views/right_panel/types"; import { ReadPinsEventId } from "../components/views/right_panel/types";
@ -31,7 +31,8 @@ export default class PinningUtils {
]; ];
/** /**
* Determines if the given event may be pinned. * Determines if the given event can be pinned.
* This is a simple check to see if the event is of a type that can be pinned.
* @param {MatrixEvent} event The event to check. * @param {MatrixEvent} event The event to check.
* @return {boolean} True if the event may be pinned, false otherwise. * @return {boolean} True if the event may be pinned, false otherwise.
*/ */
@ -62,7 +63,8 @@ export default class PinningUtils {
} }
/** /**
* Determines if the given event may be pinned or unpinned. * Determines if the given event may be pinned or unpinned by the current user.
* This checks if the user has the necessary permissions to pin or unpin the event, and if the event is pinnable.
* @param matrixClient * @param matrixClient
* @param mxEvent * @param mxEvent
*/ */
@ -77,7 +79,7 @@ export default class PinningUtils {
room room
.getLiveTimeline() .getLiveTimeline()
.getState(EventTimeline.FORWARDS) .getState(EventTimeline.FORWARDS)
?.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient) && canPinEvent(mxEvent), ?.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient) && PinningUtils.isPinnable(mxEvent),
); );
} }
@ -101,16 +103,21 @@ export default class PinningUtils {
?.getStateEvents(EventType.RoomPinnedEvents, "") ?.getStateEvents(EventType.RoomPinnedEvents, "")
?.getContent().pinned || []; ?.getContent().pinned || [];
let roomAccountDataPromise: Promise<{} | void> = Promise.resolve();
// If the event is already pinned, unpin it // If the event is already pinned, unpin it
if (pinnedIds.includes(eventId)) { if (pinnedIds.includes(eventId)) {
pinnedIds.splice(pinnedIds.indexOf(eventId), 1); pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
} else { } else {
// Otherwise, pin it // Otherwise, pin it
pinnedIds.push(eventId); pinnedIds.push(eventId);
await matrixClient.setRoomAccountData(room.roomId, ReadPinsEventId, { // We don't want to wait for the roomAccountDataPromise to resolve before sending the state event
roomAccountDataPromise = matrixClient.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []), eventId], event_ids: [...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []), eventId],
}); });
} }
await matrixClient.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned: pinnedIds }, ""); await Promise.all([
matrixClient.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned: pinnedIds }, ""),
roomAccountDataPromise,
]);
} }
} }

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { act, render, fireEvent } from "@testing-library/react"; import { act, render, fireEvent, screen, waitFor } from "@testing-library/react";
import { import {
EventType, EventType,
EventStatus, EventStatus,
@ -26,6 +26,7 @@ import {
FeatureSupport, FeatureSupport,
Thread, Thread,
EventTimeline, EventTimeline,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import MessageActionBar from "../../../../src/components/views/messages/MessageActionBar"; import MessageActionBar from "../../../../src/components/views/messages/MessageActionBar";
@ -41,6 +42,7 @@ import { IRoomState } from "../../../../src/components/structures/RoomView";
import dispatcher from "../../../../src/dispatcher/dispatcher"; import dispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { Action } from "../../../../src/dispatcher/actions"; import { Action } from "../../../../src/dispatcher/actions";
import PinningUtils from "../../../../src/utils/PinningUtils";
jest.mock("../../../../src/dispatcher/dispatcher"); jest.mock("../../../../src/dispatcher/dispatcher");
@ -119,6 +121,7 @@ describe("<MessageActionBar />", () => {
timelineRenderingType: TimelineRenderingType.Room, timelineRenderingType: TimelineRenderingType.Room,
canSendMessages: true, canSendMessages: true,
canReact: true, canReact: true,
room,
} as unknown as IRoomState; } as unknown as IRoomState;
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
render( render(
@ -476,6 +479,7 @@ describe("<MessageActionBar />", () => {
beforeEach(() => { beforeEach(() => {
// enable pin button // enable pin button
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
}); });
afterEach(() => { afterEach(() => {
@ -499,5 +503,25 @@ describe("<MessageActionBar />", () => {
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
expect(queryByLabelText("Pin")).toBeTruthy(); expect(queryByLabelText("Pin")).toBeTruthy();
}); });
it("should listen to room pinned events", async () => {
getComponent({ mxEvent: alicesMessageEvent });
expect(screen.getByLabelText("Pin")).toBeInTheDocument();
// Event is considered pinned
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true);
// Emit that the room pinned events have changed
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
roomState.emit(
RoomStateEvent.Events,
{
getType: () => EventType.RoomPinnedEvents,
} as MatrixEvent,
roomState,
null,
);
await waitFor(() => expect(screen.getByLabelText("Unpin")).toBeInTheDocument());
});
}); });
}); });

View File

@ -20,7 +20,7 @@ import { mocked } from "jest-mock";
import { createTestClient } from "../test-utils"; import { createTestClient } from "../test-utils";
import PinningUtils from "../../src/utils/PinningUtils"; import PinningUtils from "../../src/utils/PinningUtils";
import SettingsStore from "../../src/settings/SettingsStore"; import SettingsStore from "../../src/settings/SettingsStore";
import { canPinEvent, isContentActionable } from "../../src/utils/EventUtils"; import { isContentActionable } from "../../src/utils/EventUtils";
import { ReadPinsEventId } from "../../src/components/views/right_panel/types"; import { ReadPinsEventId } from "../../src/components/views/right_panel/types";
jest.mock("../../src/utils/EventUtils", () => { jest.mock("../../src/utils/EventUtils", () => {
@ -35,7 +35,6 @@ describe("PinningUtils", () => {
const userId = "@alice:example.org"; const userId = "@alice:example.org";
const mockedIsContentActionable = mocked(isContentActionable); const mockedIsContentActionable = mocked(isContentActionable);
const mockedCanPinEvent = mocked(canPinEvent);
let matrixClient: MatrixClient; let matrixClient: MatrixClient;
let room: Room; let room: Room;
@ -63,7 +62,6 @@ describe("PinningUtils", () => {
// Enable feature pinning // Enable feature pinning
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
mockedIsContentActionable.mockImplementation(() => true); mockedIsContentActionable.mockImplementation(() => true);
mockedCanPinEvent.mockImplementation(() => true);
matrixClient = createTestClient(); matrixClient = createTestClient();
room = new Room(roomId, matrixClient, userId); room = new Room(roomId, matrixClient, userId);
@ -171,8 +169,7 @@ describe("PinningUtils", () => {
}); });
test("should return false if event is not pinnable", () => { test("should return false if event is not pinnable", () => {
mockedCanPinEvent.mockReturnValue(false); const event = makePinEvent({ type: EventType.RoomCreate });
const event = makePinEvent();
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false); expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
}); });