Display composer only after isEncrypted is computed

This commit is contained in:
Florian Duros 2024-11-19 15:02:03 +01:00
parent 90cd420f4a
commit 4d4e037391
No known key found for this signature in database
GPG key ID: A5BBB4041B493F15
3 changed files with 73 additions and 52 deletions

View file

@ -239,7 +239,11 @@ interface ISendMessageComposerProps extends MatrixClientProps {
toggleStickerPickerOpen: () => void; toggleStickerPickerOpen: () => void;
} }
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> { interface SendMessageComposerState {
isReady: boolean;
}
export class SendMessageComposer extends React.Component<ISendMessageComposerProps, SendMessageComposerState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; public declare context: React.ContextType<typeof RoomContext>;
@ -257,6 +261,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
const parts = this.restoreStoredEditorState(partCreator) || []; const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator); this.model = new EditorModel(parts, partCreator);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_"); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_");
this.state = { isReady: false };
} }
public async componentDidMount(): Promise<void> { public async componentDidMount(): Promise<void> {
@ -272,6 +277,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
{ leading: true, trailing: false }, { leading: true, trailing: false },
); );
} }
this.setState({ isReady: true });
} }
public componentDidUpdate(prevProps: ISendMessageComposerProps): void { public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
@ -746,6 +752,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
if (!this.state.isReady) return null;
const threadId = const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : undefined; this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : undefined;
return ( return (

View file

@ -100,19 +100,19 @@ describe("MessageComposer", () => {
describe("for a Room", () => { describe("for a Room", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli); const room = mkStubRoom("!roomId:server", "Room 1", cli);
it("Renders a SendMessageComposer and MessageComposerButtons by default", () => { it("Renders a SendMessageComposer and MessageComposerButtons by default", async () => {
wrapAndRender({ room }); await wrapAndRender({ room });
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument(); expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
}); });
it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => { it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", async () => {
wrapAndRender({ room }, false); await wrapAndRender({ room }, false);
expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument();
expect(screen.getByText("You do not have permission to post to this room")).toBeInTheDocument(); expect(screen.getByText("You do not have permission to post to this room")).toBeInTheDocument();
}); });
it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => { it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", async () => {
wrapAndRender( await wrapAndRender(
{ room }, { room },
true, true,
false, false,
@ -135,15 +135,17 @@ describe("MessageComposer", () => {
let roomContext: IRoomState; let roomContext: IRoomState;
let resizeNotifier: ResizeNotifier; let resizeNotifier: ResizeNotifier;
beforeEach(() => { beforeEach(async () => {
jest.useFakeTimers(); jest.useFakeTimers();
resizeNotifier = { resizeNotifier = {
notifyTimelineHeightChanged: jest.fn(), notifyTimelineHeightChanged: jest.fn(),
} as unknown as ResizeNotifier; } as unknown as ResizeNotifier;
roomContext = wrapAndRender({ roomContext = (
await wrapAndRender({
room, room,
resizeNotifier, resizeNotifier,
}).roomContext; })
).roomContext;
}); });
it("should call notifyTimelineHeightChanged() for the same context", () => { it("should call notifyTimelineHeightChanged() for the same context", () => {
@ -185,8 +187,8 @@ describe("MessageComposer", () => {
[true, false].forEach((value: boolean) => { [true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => { describe(`when ${setting} = ${value}`, () => {
beforeEach(async () => { beforeEach(async () => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
wrapAndRender({ room }); await wrapAndRender({ room });
await act(async () => { await act(async () => {
await userEvent.click(screen.getByLabelText("More options")); await userEvent.click(screen.getByLabelText("More options"));
}); });
@ -230,14 +232,14 @@ describe("MessageComposer", () => {
}); });
}); });
it("should not render the send button", () => { it("should not render the send button", async () => {
wrapAndRender({ room }); await wrapAndRender({ room });
expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument();
}); });
describe("when a message has been entered", () => { describe("when a message has been entered", () => {
beforeEach(async () => { beforeEach(async () => {
const renderResult = wrapAndRender({ room }).renderResult; const renderResult = (await wrapAndRender({ room })).renderResult;
await addTextToComposerRTL(renderResult, "Hello"); await addTextToComposerRTL(renderResult, "Hello");
}); });
@ -259,7 +261,7 @@ describe("MessageComposer", () => {
describe("when a non-resize event occurred in UIStore", () => { describe("when a non-resize event occurred in UIStore", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }); await wrapAndRender({ room });
await openStickerPicker(); await openStickerPicker();
resizeCallback("test", {}); resizeCallback("test", {});
}); });
@ -271,7 +273,7 @@ describe("MessageComposer", () => {
describe("when a resize to narrow event occurred in UIStore", () => { describe("when a resize to narrow event occurred in UIStore", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }, true, true); await wrapAndRender({ room }, true, true);
await openStickerPicker(); await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {}); resizeCallback(UI_EVENTS.Resize, {});
}); });
@ -293,7 +295,7 @@ describe("MessageComposer", () => {
describe("when a resize to non-narrow event occurred in UIStore", () => { describe("when a resize to non-narrow event occurred in UIStore", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }, true, false); await wrapAndRender({ room }, true, false);
await openStickerPicker(); await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {}); resizeCallback(UI_EVENTS.Resize, {});
}); });
@ -315,13 +317,13 @@ describe("MessageComposer", () => {
}); });
describe("when not replying to an event", () => { describe("when not replying to an event", () => {
it("should pass the expected placeholder to SendMessageComposer", () => { it("should pass the expected placeholder to SendMessageComposer", async () => {
wrapAndRender({ room }); await wrapAndRender({ room });
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument(); expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
}); });
it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => { it("and an e2e status it should pass the expected placeholder to SendMessageComposer", async () => {
wrapAndRender({ await wrapAndRender({
room, room,
e2eStatus: E2EStatus.Normal, e2eStatus: E2EStatus.Normal,
}); });
@ -334,8 +336,8 @@ describe("MessageComposer", () => {
let props: Partial<React.ComponentProps<typeof MessageComposer>>; let props: Partial<React.ComponentProps<typeof MessageComposer>>;
const checkPlaceholder = (expected: string) => { const checkPlaceholder = (expected: string) => {
it("should pass the expected placeholder to SendMessageComposer", () => { it("should pass the expected placeholder to SendMessageComposer", async () => {
wrapAndRender(props); await wrapAndRender(props);
expect(screen.getByLabelText(expected)).toBeInTheDocument(); expect(screen.getByLabelText(expected)).toBeInTheDocument();
}); });
}; };
@ -393,7 +395,7 @@ describe("MessageComposer", () => {
describe("when clicking start a voice message", () => { describe("when clicking start a voice message", () => {
beforeEach(async () => { beforeEach(async () => {
wrapAndRender({ room }); await wrapAndRender({ room });
await startVoiceMessage(); await startVoiceMessage();
await flushPromises(); await flushPromises();
}); });
@ -406,7 +408,7 @@ describe("MessageComposer", () => {
describe("when recording a voice broadcast and trying to start a voice message", () => { describe("when recording a voice broadcast and trying to start a voice message", () => {
beforeEach(async () => { beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started); setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
wrapAndRender({ room }); await wrapAndRender({ room });
await startVoiceMessage(); await startVoiceMessage();
await waitEnoughCyclesForModal(); await waitEnoughCyclesForModal();
}); });
@ -420,7 +422,7 @@ describe("MessageComposer", () => {
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => { describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
beforeEach(async () => { beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped); setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
wrapAndRender({ room }); await wrapAndRender({ room });
await startVoiceMessage(); await startVoiceMessage();
await waitEnoughCyclesForModal(); await waitEnoughCyclesForModal();
}); });
@ -436,7 +438,7 @@ describe("MessageComposer", () => {
const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!); const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
it("should not show the stickers button", async () => { it("should not show the stickers button", async () => {
wrapAndRender({ room: localRoom }); await wrapAndRender({ room: localRoom });
await act(async () => { await act(async () => {
await userEvent.click(screen.getByLabelText("More options")); await userEvent.click(screen.getByLabelText("More options"));
}); });
@ -448,7 +450,7 @@ describe("MessageComposer", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli); const room = mkStubRoom("!roomId:server", "Room 1", cli);
const messageText = "Test Text"; const messageText = "Test Text";
await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const { renderResult, rawComponent } = wrapAndRender({ room }); const { renderResult, rawComponent } = await wrapAndRender({ room }, true, false, undefined, true);
const { unmount, rerender } = renderResult; const { unmount, rerender } = renderResult;
await act(async () => { await act(async () => {
@ -490,11 +492,12 @@ describe("MessageComposer", () => {
}, 10000); }, 10000);
}); });
function wrapAndRender( async function wrapAndRender(
props: Partial<React.ComponentProps<typeof MessageComposer>> = {}, props: Partial<React.ComponentProps<typeof MessageComposer>> = {},
canSendMessages = true, canSendMessages = true,
narrow = false, narrow = false,
tombstone?: MatrixEvent, tombstone?: MatrixEvent,
ignoreWaitForRender = false,
) { ) {
const mockClient = MatrixClientPeg.safeGet(); const mockClient = MatrixClientPeg.safeGet();
const roomId = "myroomid"; const roomId = "myroomid";
@ -527,9 +530,15 @@ function wrapAndRender(
</RoomContext.Provider> </RoomContext.Provider>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
const renderResult = render(getRawComponent(props, roomContext, mockClient));
if (!ignoreWaitForRender && canSendMessages && !tombstone) {
await waitFor(() => expect(renderResult.getByRole("textbox")).toBeInTheDocument());
}
return { return {
rawComponent: getRawComponent(props, roomContext, mockClient), rawComponent: getRawComponent(props, roomContext, mockClient),
renderResult: render(getRawComponent(props, roomContext, mockClient)), renderResult,
roomContext, roomContext,
}; };
} }

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, waitFor } from "jest-matrix-react"; import { fireEvent, render, waitFor, screen } from "jest-matrix-react";
import { IContent, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; import { IContent, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@ -369,12 +369,15 @@ describe("<SendMessageComposer/>", () => {
</RoomContext.Provider> </RoomContext.Provider>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { const getComponent = async (props = {}, roomContext = defaultRoomContext, client = mockClient) => {
return render(getRawComponent(props, roomContext, client)); const renderResult = render(getRawComponent(props, roomContext, client));
// Wait for the composer to be rendered
await waitFor(() => expect(screen.getByRole("textbox")).toBeInTheDocument());
return renderResult;
}; };
it("renders text and placeholder correctly", () => { it("renders text and placeholder correctly", async () => {
const { container } = getComponent({ placeholder: "placeholder string" }); const { container } = await getComponent({ placeholder: "placeholder string" });
expect(container.querySelectorAll('[aria-label="placeholder string"]')).toHaveLength(1); expect(container.querySelectorAll('[aria-label="placeholder string"]')).toHaveLength(1);
@ -383,9 +386,9 @@ describe("<SendMessageComposer/>", () => {
expect(container.textContent).toBe("Test Text"); expect(container.textContent).toBe("Test Text");
}); });
it("correctly persists state to and from localStorage", () => { it("correctly persists state to and from localStorage", async () => {
const props = { replyToEvent: mockEvent }; const props = { replyToEvent: mockEvent };
const { container, unmount, rerender } = getComponent(props); const { container, unmount, rerender } = await getComponent(props);
addTextToComposer(container, "Test Text"); addTextToComposer(container, "Test Text");
@ -403,7 +406,7 @@ describe("<SendMessageComposer/>", () => {
// ensure the correct model is re-loaded // ensure the correct model is re-loaded
rerender(getRawComponent(props)); rerender(getRawComponent(props));
expect(container.textContent).toBe("Test Text"); await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent("Test Text"));
expect(spyDispatcher).toHaveBeenCalledWith({ expect(spyDispatcher).toHaveBeenCalledWith({
action: "reply_to_event", action: "reply_to_event",
event: mockEvent, event: mockEvent,
@ -417,8 +420,8 @@ describe("<SendMessageComposer/>", () => {
expect(container.textContent).toBe(""); expect(container.textContent).toBe("");
}); });
it("persists state correctly without replyToEvent onbeforeunload", () => { it("persists state correctly without replyToEvent onbeforeunload", async () => {
const { container } = getComponent(); const { container } = await getComponent();
addTextToComposer(container, "Hello World"); addTextToComposer(container, "Hello World");
@ -437,7 +440,7 @@ describe("<SendMessageComposer/>", () => {
it("persists to session history upon sending", async () => { it("persists to session history upon sending", async () => {
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent({ replyToEvent: mockEvent }); const { container } = await getComponent({ replyToEvent: mockEvent });
addTextToComposer(container, "This is a message"); addTextToComposer(container, "This is a message");
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
@ -458,7 +461,7 @@ describe("<SendMessageComposer/>", () => {
}); });
}); });
it("correctly sends a message", () => { it("correctly sends a message", async () => {
mocked(doMaybeLocalRoomAction).mockImplementation( mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => { <T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {
return fn(roomId); return fn(roomId);
@ -466,7 +469,7 @@ describe("<SendMessageComposer/>", () => {
); );
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent(); const { container } = await getComponent();
addTextToComposer(container, "test message"); addTextToComposer(container, "test message");
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
@ -495,7 +498,7 @@ describe("<SendMessageComposer/>", () => {
}); });
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent({ replyToEvent }); const { container } = await getComponent({ replyToEvent });
addTextToComposer(container, "/tableflip"); addTextToComposer(container, "/tableflip");
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
@ -516,7 +519,7 @@ describe("<SendMessageComposer/>", () => {
); );
}); });
it("shows chat effects on message sending", () => { it("shows chat effects on message sending", async () => {
mocked(doMaybeLocalRoomAction).mockImplementation( mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => { <T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {
return fn(roomId); return fn(roomId);
@ -524,7 +527,7 @@ describe("<SendMessageComposer/>", () => {
); );
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent(); const { container } = await getComponent();
addTextToComposer(container, "🎉"); addTextToComposer(container, "🎉");
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
@ -538,7 +541,7 @@ describe("<SendMessageComposer/>", () => {
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: `effects.confetti` }); expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: `effects.confetti` });
}); });
it("not to send chat effects on message sending for threads", () => { it("not to send chat effects on message sending for threads", async () => {
mocked(doMaybeLocalRoomAction).mockImplementation( mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => { <T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {
return fn(roomId); return fn(roomId);
@ -546,7 +549,7 @@ describe("<SendMessageComposer/>", () => {
); );
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent({ const { container } = await getComponent({
relation: { relation: {
rel_type: "m.thread", rel_type: "m.thread",
event_id: "$yolo", event_id: "$yolo",
@ -615,7 +618,8 @@ describe("<SendMessageComposer/>", () => {
<SendMessageComposer room={room} toggleStickerPickerOpen={jest.fn()} /> <SendMessageComposer room={room} toggleStickerPickerOpen={jest.fn()} />
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
// Wait for the composer to be rendered
await waitFor(() => expect(screen.getByRole("textbox")).toBeInTheDocument());
const composer = container.querySelector<HTMLDivElement>(".mx_BasicMessageComposer_input")!; const composer = container.querySelector<HTMLDivElement>(".mx_BasicMessageComposer_input")!;
// Does not trigger on keydown as that'll cause false negatives for global shortcuts // Does not trigger on keydown as that'll cause false negatives for global shortcuts