Display composer only after `isEncrypted` is computed

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

View File

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

View File

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