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

Commit 5bfbca9

Browse files
authored
Migrate all pinning checks and actions into PinningUtils (#12964)
1 parent 2639923 commit 5bfbca9

File tree

8 files changed

+140
-61
lines changed

8 files changed

+140
-61
lines changed

src/components/views/context_menus/MessageContextMenu.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
177177
this.props.mxEvent.getType() !== EventType.RoomServerAcl &&
178178
this.props.mxEvent.getType() !== EventType.RoomEncryption;
179179

180-
const canPin = PinningUtils.canPinOrUnpin(cli, this.props.mxEvent);
180+
const canPin = PinningUtils.canPin(cli, this.props.mxEvent) || PinningUtils.canUnpin(cli, this.props.mxEvent);
181181

182182
this.setState({ canRedact, canPin });
183183
};

src/components/views/dialogs/UnpinAllDialog.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
import React, { JSX } from "react";
1818
import { Button, Text } from "@vector-im/compound-web";
19-
import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
19+
import { MatrixClient } from "matrix-js-sdk/src/matrix";
2020
import { logger } from "matrix-js-sdk/src/logger";
2121

2222
import BaseDialog from "../dialogs/BaseDialog";
2323
import { _t } from "../../../languageHandler";
24+
import PinningUtils from "../../../utils/PinningUtils.ts";
2425

2526
/**
2627
* Properties for {@link UnpinAllDialog}.
@@ -59,7 +60,7 @@ export function UnpinAllDialog({ matrixClient, roomId, onFinished }: UnpinAllDia
5960
destructive={true}
6061
onClick={async () => {
6162
try {
62-
await matrixClient.sendStateEvent(roomId, EventType.RoomPinnedEvents, { pinned: [] }, "");
63+
await PinningUtils.unpinAllEvents(matrixClient, roomId);
6364
} catch (e) {
6465
logger.error("Failed to unpin all events:", e);
6566
}

src/components/views/messages/MessageActionBar.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,10 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
432432
);
433433
}
434434

435-
if (PinningUtils.canPinOrUnpin(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
435+
if (
436+
PinningUtils.canPin(MatrixClientPeg.safeGet(), this.props.mxEvent) ||
437+
PinningUtils.canUnpin(MatrixClientPeg.safeGet(), this.props.mxEvent)
438+
) {
436439
const isPinned = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent);
437440
toolbarOpts.push(
438441
<RovingAccessibleButton

src/components/views/right_panel/PinnedMessagesCard.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import React, { useCallback, useEffect, JSX } from "react";
18-
import { Room, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
18+
import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix";
1919
import { Button, Separator } from "@vector-im/compound-web";
2020
import classNames from "classnames";
2121
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin";
@@ -35,6 +35,7 @@ import Modal from "../../../Modal";
3535
import { UnpinAllDialog } from "../dialogs/UnpinAllDialog";
3636
import EmptyState from "./EmptyState";
3737
import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
38+
import PinningUtils from "../../../utils/PinningUtils.ts";
3839

3940
/**
4041
* List the pinned messages in a room inside a Card.
@@ -141,10 +142,9 @@ function PinnedMessages({ events, room, permalinkCreator }: PinnedMessagesProps)
141142

142143
/**
143144
* Whether the client can unpin events from the room.
145+
* Listen to room state to update this value.
144146
*/
145-
const canUnpin = useRoomState(room, (state) =>
146-
state.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient),
147-
);
147+
const canUnpin = useRoomState(room, () => PinningUtils.userHasPinOrUnpinPermission(matrixClient, room));
148148

149149
/**
150150
* Opens the unpin all dialog.

src/components/views/rooms/PinnedEventTile.tsx

+5-17
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { getForwardableEvent } from "../../../events";
4141
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
4242
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
4343
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
44+
import PinningUtils from "../../../utils/PinningUtils.ts";
4445

4546
const AVATAR_SIZE = "32px";
4647

@@ -162,30 +163,17 @@ function PinMenu({ event, room, permalinkCreator }: PinMenuProps): JSX.Element {
162163

163164
/**
164165
* Whether the client can unpin the event.
165-
* Pin and unpin are using the same permission.
166+
* If the room state change, we want to check again the permission
166167
*/
167-
const canUnpin = useRoomState(room, (state) =>
168-
state.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient),
169-
);
168+
const canUnpin = useRoomState(room, () => PinningUtils.canUnpin(matrixClient, event));
170169

171170
/**
172171
* Unpin the event.
173172
* @param event
174173
*/
175174
const onUnpin = useCallback(async (): Promise<void> => {
176-
const pinnedEvents = room
177-
.getLiveTimeline()
178-
.getState(EventTimeline.FORWARDS)
179-
?.getStateEvents(EventType.RoomPinnedEvents, "");
180-
if (pinnedEvents?.getContent()?.pinned) {
181-
const pinned = pinnedEvents.getContent().pinned;
182-
const index = pinned.indexOf(event.getId());
183-
if (index !== -1) {
184-
pinned.splice(index, 1);
185-
await matrixClient.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
186-
}
187-
}
188-
}, [event, room, matrixClient]);
175+
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
176+
}, [event, matrixClient]);
189177

190178
const contentActionable = isContentActionable(event);
191179
// Get the forwardable event for the given event

src/utils/PinningUtils.ts

+44-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix";
17+
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline, Room } from "matrix-js-sdk/src/matrix";
1818

1919
import { isContentActionable } from "./EventUtils";
2020
import SettingsStore from "../settings/SettingsStore";
@@ -71,23 +71,53 @@ export default class PinningUtils {
7171
}
7272

7373
/**
74-
* Determines if the given event may be pinned or unpinned by the current user.
75-
* This checks if the user has the necessary permissions to pin or unpin the event, and if the event is pinnable.
74+
* Determines if the given event may be pinned or unpinned by the current user
75+
* It doesn't check if the event is pinnable or unpinnable.
7676
* @param matrixClient
7777
* @param mxEvent
78+
* @private
7879
*/
79-
public static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
80+
private static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
8081
if (!SettingsStore.getValue("feature_pinning")) return false;
8182
if (!isContentActionable(mxEvent)) return false;
8283

8384
const room = matrixClient.getRoom(mxEvent.getRoomId());
8485
if (!room) return false;
8586

87+
return PinningUtils.userHasPinOrUnpinPermission(matrixClient, room);
88+
}
89+
90+
/**
91+
* Determines if the given event may be pinned by the current user.
92+
* This checks if the user has the necessary permissions to pin or unpin the event, and if the event is pinnable.
93+
* @param matrixClient
94+
* @param mxEvent
95+
*/
96+
public static canPin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
97+
return PinningUtils.canPinOrUnpin(matrixClient, mxEvent) && PinningUtils.isPinnable(mxEvent);
98+
}
99+
100+
/**
101+
* Determines if the given event may be unpinned by the current user.
102+
* This checks if the user has the necessary permissions to pin or unpin the event, and if the event is unpinnable.
103+
* @param matrixClient
104+
* @param mxEvent
105+
*/
106+
public static canUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
107+
return PinningUtils.canPinOrUnpin(matrixClient, mxEvent) && PinningUtils.isUnpinnable(mxEvent);
108+
}
109+
110+
/**
111+
* Determines if the current user has permission to pin or unpin events in the given room.
112+
* @param matrixClient
113+
* @param room
114+
*/
115+
public static userHasPinOrUnpinPermission(matrixClient: MatrixClient, room: Room): boolean {
86116
return Boolean(
87117
room
88118
.getLiveTimeline()
89119
.getState(EventTimeline.FORWARDS)
90-
?.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient) && PinningUtils.isPinnable(mxEvent),
120+
?.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient),
91121
);
92122
}
93123

@@ -128,4 +158,13 @@ export default class PinningUtils {
128158
roomAccountDataPromise,
129159
]);
130160
}
161+
162+
/**
163+
* Unpin all events in the given room.
164+
* @param matrixClient
165+
* @param roomId
166+
*/
167+
public static async unpinAllEvents(matrixClient: MatrixClient, roomId: string): Promise<void> {
168+
await matrixClient.sendStateEvent(roomId, EventType.RoomPinnedEvents, { pinned: [] }, "");
169+
}
131170
}

test/components/views/rooms/PinnedEventTile-test.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import dis from "../../../../src/dispatcher/dispatcher";
2727
import { Action } from "../../../../src/dispatcher/actions";
2828
import { getForwardableEvent } from "../../../../src/events";
2929
import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog";
30+
import SettingsStore from "../../../../src/settings/SettingsStore.ts";
3031

3132
jest.mock("../../../../src/components/views/dialogs/ConfirmRedactDialog", () => ({
3233
createRedactEventDialog: jest.fn(),
@@ -43,7 +44,10 @@ describe("<PinnedEventTile />", () => {
4344
mockClient = stubClient();
4445
room = new Room(roomId, mockClient, userId);
4546
permalinkCreator = new RoomPermalinkCreator(room);
47+
mockClient.getRoom = jest.fn().mockReturnValue(room);
4648
jest.spyOn(dis, "dispatch").mockReturnValue(undefined);
49+
// Enable feature_pinning
50+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
4751
});
4852

4953
/**

test/utils/PinningUtils-test.ts

+75-31
Original file line numberDiff line numberDiff line change
@@ -147,49 +147,65 @@ describe("PinningUtils", () => {
147147
});
148148
});
149149

150-
describe("canPinOrUnpin", () => {
151-
test("should return false if pinning is disabled", () => {
152-
// Disable feature pinning
153-
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
154-
const event = makePinEvent();
150+
describe("canPin & canUnpin", () => {
151+
describe("canPin", () => {
152+
test("should return false if pinning is disabled", () => {
153+
// Disable feature pinning
154+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
155+
const event = makePinEvent();
156+
157+
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
158+
});
155159

156-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
157-
});
160+
test("should return false if event is not actionable", () => {
161+
mockedIsContentActionable.mockImplementation(() => false);
162+
const event = makePinEvent();
158163

159-
test("should return false if event is not actionable", () => {
160-
mockedIsContentActionable.mockImplementation(() => false);
161-
const event = makePinEvent();
164+
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
165+
});
162166

163-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
164-
});
167+
test("should return false if no room", () => {
168+
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
169+
const event = makePinEvent();
165170

166-
test("should return false if no room", () => {
167-
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
168-
const event = makePinEvent();
171+
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
172+
});
169173

170-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
171-
});
174+
test("should return false if client cannot send state event", () => {
175+
jest.spyOn(
176+
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
177+
"mayClientSendStateEvent",
178+
).mockReturnValue(false);
179+
const event = makePinEvent();
172180

173-
test("should return false if client cannot send state event", () => {
174-
jest.spyOn(
175-
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
176-
"mayClientSendStateEvent",
177-
).mockReturnValue(false);
178-
const event = makePinEvent();
181+
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
182+
});
179183

180-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
181-
});
184+
test("should return false if event is not pinnable", () => {
185+
const event = makePinEvent({ type: EventType.RoomCreate });
182186

183-
test("should return false if event is not pinnable", () => {
184-
const event = makePinEvent({ type: EventType.RoomCreate });
187+
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
188+
});
185189

186-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
190+
test("should return true if all conditions are met", () => {
191+
const event = makePinEvent();
192+
193+
expect(PinningUtils.canPin(matrixClient, event)).toBe(true);
194+
});
187195
});
188196

189-
test("should return true if all conditions are met", () => {
190-
const event = makePinEvent();
197+
describe("canUnpin", () => {
198+
test("should return false if event is not unpinnable", () => {
199+
const event = makePinEvent({ type: EventType.RoomCreate });
191200

192-
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(true);
201+
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(false);
202+
});
203+
204+
test("should return true if all conditions are met", () => {
205+
const event = makePinEvent();
206+
207+
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(true);
208+
});
193209
});
194210
});
195211

@@ -258,4 +274,32 @@ describe("PinningUtils", () => {
258274
);
259275
});
260276
});
277+
278+
describe("userHasPinOrUnpinPermission", () => {
279+
test("should return true if user can pin or unpin", () => {
280+
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(true);
281+
});
282+
283+
test("should return false if client cannot send state event", () => {
284+
jest.spyOn(
285+
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
286+
"mayClientSendStateEvent",
287+
).mockReturnValue(false);
288+
289+
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(false);
290+
});
291+
});
292+
293+
describe("unpinAllEvents", () => {
294+
it("should unpin all events in the given room", async () => {
295+
await PinningUtils.unpinAllEvents(matrixClient, roomId);
296+
297+
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
298+
roomId,
299+
EventType.RoomPinnedEvents,
300+
{ pinned: [] },
301+
"",
302+
);
303+
});
304+
});
261305
});

0 commit comments

Comments
 (0)