Module API for adding new file upload mechanisms (#33355)

* Initial reword of upload to MVVM.

* Update tests

* More incremental improvements

* Refactor tests to use helper method for composer uploads.

* Add drag and drop tests

* lint

* Add commentary

* fixup test

* More precise selector

* Retarget uploads

* lint

* fixup

* one more type

* update snap

* Fixup composerUploadFiles

* fix import

* lint

* Copy and paste fixes too

* Add tests for pasting

* Add tests for pasting files.

* Remove redundant fn

* rm comment

* tidy up

* Test cleanup

* More clean up

* another fix

* Begin fleshing out

* Park changes

* More stuff

* Use condensed version

* Cleanup tests

* more cleaning

* last bity

* Add a test for the composer

* Park up changes

* Rewrite Measured to be a functional component

* Add tests to cover narrow viewports

* lint

* breakpoint is optional

* Cleanup

* Support narrow mode

* fixup

* begone

* Provide default value

* add label

* fixup test

* update copyright

* cleanup

* Be a bit more lazy with FileDropTarget

* remove a debug statement

* Fixup

* fix two snaps

* Update screenshot

* and the other one

* Update snaps

* unfake CIDER

* update screens again

* remove extra test

* Undo accidental snapshots

* Bit of tidyup

* fixup

* even more tidyup

* may drag and drop file

* tidy up again

* snap snap snap

* Use load to make sonarQube happy

* Bunch of refactors

* More cleanup

* cleanup debug code

* tweaks

* remove a test we no longer need

* make it happy

* fix import

* fixup

* Update snaps

* typo

* one off

* Add tests

* lint

* remove only

* Reduce screenshot scope

* fix snapshot usage

* cleanup
This commit is contained in:
Will Hunt
2026-05-18 21:41:38 +00:00
committed by GitHub
parent 66b739fea9
commit 02b6520f09
30 changed files with 1627 additions and 831 deletions
@@ -10,26 +10,21 @@ import { render, fireEvent } from "jest-matrix-react";
import { useMockedViewModel } from "@element-hq/web-shared-components";
import FileDropTarget from "../../../../src/components/structures/FileDropTarget.tsx";
import {
RoomUploadContext,
type RoomUploadViewActions,
type RoomUploadViewModel,
type RoomUploadViewSnapshot,
} from "../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
import { RoomUploadContext, type RoomUploadViewModel } from "../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
function FileDropTargetWrapped({
element,
snapshot,
actions,
mayDragAndDropFile = false,
onFileDrop,
}: {
element: HTMLDivElement;
snapshot: RoomUploadViewSnapshot;
actions: Partial<RoomUploadViewActions>;
mayDragAndDropFile?: boolean;
onFileDrop: RoomUploadViewModel["initiateViaDataTransfer"];
}) {
const mockVm = useMockedViewModel<RoomUploadViewSnapshot, RoomUploadViewActions>(
snapshot,
actions as RoomUploadViewActions,
);
const mockVm = useMockedViewModel<
{ mayDragAndDropFile: boolean },
Pick<RoomUploadViewModel, "initiateViaDataTransfer">
>({ mayDragAndDropFile }, { initiateViaDataTransfer: onFileDrop });
return (
<RoomUploadContext.Provider value={mockVm as RoomUploadViewModel}>
<FileDropTarget parent={element} />
@@ -43,11 +38,7 @@ describe("FileDropTarget", () => {
const onFileDrop = jest.fn();
const { asFragment } = render(
<FileDropTargetWrapped
element={element}
snapshot={{ mayUpload: true }}
actions={{ initiateViaDataTransfer: onFileDrop }}
/>,
<FileDropTargetWrapped element={element} mayDragAndDropFile onFileDrop={onFileDrop} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -56,11 +47,7 @@ describe("FileDropTarget", () => {
const element = document.createElement("div");
const onFileDrop = jest.fn();
const { asFragment } = render(
<FileDropTargetWrapped
element={element}
snapshot={{ mayUpload: true }}
actions={{ initiateViaDataTransfer: onFileDrop }}
/>,
<FileDropTargetWrapped element={element} mayDragAndDropFile onFileDrop={onFileDrop} />,
);
fireEvent.dragEnter(element, {
dataTransfer: {
@@ -73,13 +60,7 @@ describe("FileDropTarget", () => {
it("should not render drop file prompt on mouse over with file if permissions do not allow", () => {
const element = document.createElement("div");
const onFileDrop = jest.fn();
const { asFragment } = render(
<FileDropTargetWrapped
element={element}
snapshot={{ mayUpload: false }}
actions={{ initiateViaDataTransfer: onFileDrop }}
/>,
);
const { asFragment } = render(<FileDropTargetWrapped element={element} onFileDrop={onFileDrop} />);
fireEvent.dragEnter(element, {
dataTransfer: {
types: ["Files"],
@@ -1079,27 +1079,6 @@ describe("RoomView", () => {
});
describe("handles Action.ComposerInsert", () => {
it("redispatches an empty composerType, timelineRenderingType with the current state", async () => {
await mountRoomView();
const promise = untilDispatch((payload) => {
try {
expect(payload).toEqual({
action: Action.ComposerInsert,
text: "Hello world",
timelineRenderingType: TimelineRenderingType.Room,
composerType: ComposerType.Send,
});
} catch {
return false;
}
return true;
}, defaultDispatcher);
defaultDispatcher.dispatch({
action: Action.ComposerInsert,
text: "Hello world",
} satisfies ComposerInsertPayload);
await promise;
});
it("redispatches an empty composerType with the current state", async () => {
await mountRoomView();
const promise = untilDispatch((payload) => {
@@ -212,7 +212,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</div>
</div>
<div
aria-labelledby="_r_1cl_"
aria-labelledby="_r_1dt_"
class="_banner_n7ud0_8"
data-type="critical"
role="status"
@@ -238,7 +238,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
>
<p
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _title_mcq5y_24"
id="_r_1cl_"
id="_r_1dt_"
>
Could not start a chat with this user
</p>
@@ -423,7 +423,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_19j_"
aria-labelledby="_r_1an_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -608,24 +608,32 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_1bs_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -702,7 +710,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</span>
<svg
aria-label="New members see history"
aria-labelledby="_r_1cf_"
aria-labelledby="_r_1dn_"
class="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -981,24 +989,32 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_1dc_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -1599,24 +1615,32 @@ exports[`RoomView should hide the header when hideHeader=true 1`] = `
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_27_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -1724,7 +1748,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_5e_"
aria-labelledby="_r_5i_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -1751,7 +1775,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_5j_"
aria-labelledby="_r_5n_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -1766,7 +1790,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
</button>
<button
aria-label="Threads"
aria-labelledby="_r_5o_"
aria-labelledby="_r_5s_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -1793,7 +1817,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
</button>
<button
aria-label="Room info"
aria-labelledby="_r_5t_"
aria-labelledby="_r_61_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -1823,7 +1847,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
>
<div
aria-label="0 members"
aria-labelledby="_r_62_"
aria-labelledby="_r_66_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1890,7 +1914,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_6e_"
aria-labelledby="_r_6i_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -2075,24 +2099,32 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_7n_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -2200,7 +2232,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_2g_"
aria-labelledby="_r_2i_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2227,7 +2259,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_2l_"
aria-labelledby="_r_2n_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2242,7 +2274,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="_r_2q_"
aria-labelledby="_r_2s_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -2269,7 +2301,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="_r_2v_"
aria-labelledby="_r_31_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -2299,7 +2331,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
>
<div
aria-label="0 members"
aria-labelledby="_r_34_"
aria-labelledby="_r_36_"
class="mx_AccessibleButton mx_FacePile mx_FacePile_toggled"
role="button"
tabindex="0"
@@ -2366,7 +2398,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_3g_"
aria-labelledby="_r_3i_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -2551,24 +2583,32 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_4n_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -2676,7 +2716,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_fc_"
aria-labelledby="_r_fo_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2703,7 +2743,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_fh_"
aria-labelledby="_r_ft_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2718,7 +2758,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Threads"
aria-labelledby="_r_fm_"
aria-labelledby="_r_g2_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -2745,7 +2785,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Room info"
aria-labelledby="_r_fr_"
aria-labelledby="_r_g7_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -2775,7 +2815,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
>
<div
aria-label="0 members"
aria-labelledby="_r_g0_"
aria-labelledby="_r_gc_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -2881,7 +2921,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</span>
<svg
aria-label="New members see history"
aria-labelledby="_r_gc_"
aria-labelledby="_r_go_"
class="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -2914,7 +2954,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_fc_"
aria-labelledby="_r_fo_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2941,7 +2981,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_fh_"
aria-labelledby="_r_ft_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -2956,7 +2996,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Threads"
aria-labelledby="_r_fm_"
aria-labelledby="_r_g2_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -2983,7 +3023,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Room info"
aria-labelledby="_r_fr_"
aria-labelledby="_r_g7_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -3013,7 +3053,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
>
<div
aria-label="0 members"
aria-labelledby="_r_g0_"
aria-labelledby="_r_gc_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -3082,7 +3122,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
tabindex="0"
>
<div
aria-labelledby="_r_gh_"
aria-labelledby="_r_gt_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
data-testid="e2e-icon"
style="width: 12px; height: 12px;"
@@ -3275,24 +3315,32 @@ exports[`RoomView should not display the timeline when the room encryption is lo
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_i2_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -3418,7 +3466,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Chat"
aria-labelledby="_r_ks_"
aria-labelledby="_r_lc_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -3445,7 +3493,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="_r_l1_"
aria-labelledby="_r_lh_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -3472,7 +3520,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="_r_l6_"
aria-labelledby="_r_lm_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@@ -3502,7 +3550,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<div
aria-label="0 members"
aria-labelledby="_r_lb_"
aria-labelledby="_r_lr_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -3587,7 +3635,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</p>
</div>
<button
aria-labelledby="_r_lk_"
aria-labelledby="_r_m4_"
class="_icon-button_1215g_8"
data-kind="secondary"
data-testid="base-card-close-button"
@@ -3647,7 +3695,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="_r_lp_"
aria-labelledby="_r_m9_"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -3832,24 +3880,32 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
/>
</svg>
</div>
<div
<button
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button"
aria-labelledby="_r_ne_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 26px;"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</div>
</button>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
@@ -35,11 +35,8 @@ import { addTextToComposerRTL } from "../../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { Action } from "../../../../../src/dispatcher/actions";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import {
RoomUploadContext,
type RoomUploadViewModel,
} from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
import { TimelineRenderingType, type RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
const openStickerPicker = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("More options"));
@@ -462,7 +459,8 @@ function wrapAndRender(
canSendMessages,
tombstone,
narrow,
} as unknown as RoomContextType;
timelineRenderingType: TimelineRenderingType.Room,
} satisfies Partial<RoomContextType> as RoomContextType;
const defaultProps = {
room,
@@ -473,9 +471,9 @@ function wrapAndRender(
const getRawComponent = (props = {}, context = roomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...context}>
<RoomUploadContext.Provider value={{} as RoomUploadViewModel}>
<RoomUploadContextProvider>
<MessageComposer {...defaultProps} {...props} />
</RoomUploadContext.Provider>
</RoomUploadContextProvider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
@@ -5,20 +5,79 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
import { Action } from "../../../src/dispatcher/actions";
import type { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
import { ComposerApi } from "../../../src/modules/ComposerApi";
import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
import { type ComposerInsertPayload, ComposerType } from "../../../src/dispatcher/payloads/ComposerInsertPayload";
import { ComposerApi, ModuleComposerApiEvents } from "../../../src/modules/ComposerApi";
import type { ComposerInsertFilesPayload } from "../../../src/dispatcher/payloads/ComposerInsertFilePayload";
describe("ComposerApi", () => {
it("should be able to insert text via insertTextIntoComposer()", () => {
const dispatcher = {
dispatch: jest.fn(),
} as unknown as MatrixDispatcher;
const api = new ComposerApi(dispatcher);
api.insertPlaintextIntoComposer("Hello world");
expect(dispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ComposerInsert,
text: "Hello world",
describe("insertPlaintextIntoComposer()", () => {
it("should be able to insert text", () => {
const dispatcher = {
dispatch: jest.fn(),
} as unknown as MatrixDispatcher;
const api = new ComposerApi(dispatcher);
api.insertPlaintextIntoComposer("Hello world", { view: "room" });
expect(dispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ComposerInsert,
text: "Hello world",
timelineRenderingType: TimelineRenderingType.Room,
composerType: ComposerType.Send,
} satisfies ComposerInsertPayload);
});
it("throws if called with an invalid view", () => {
const api = new ComposerApi(new MatrixDispatcher());
expect(() => api.insertPlaintextIntoComposer("text", { view: "bleh" as "room" })).toThrow(
"Invalid view 'bleh'",
);
});
});
describe("openFileUploadConfirmation()", () => {
it("should be able to initiate a file upload", () => {
const dispatcher = {
dispatch: jest.fn(),
} as unknown as MatrixDispatcher;
const api = new ComposerApi(dispatcher);
const files = [new File(["test"], "test.txt")];
api.openFileUploadConfirmation(files, { view: "room" });
expect(dispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ComposerFileInsert,
files: files,
timelineRenderingType: TimelineRenderingType.Room,
} satisfies ComposerInsertFilesPayload);
});
it("throws if called with an invalid view", () => {
const api = new ComposerApi(new MatrixDispatcher());
const files = [new File(["test"], "test.txt")];
expect(() => api.openFileUploadConfirmation(files, { view: "bleh" as "room" })).toThrow(
"Invalid view 'bleh'",
);
});
});
describe("addFileUploadOption()", () => {
it("should be able to add a file upload option", () => {
const api = new ComposerApi(new MatrixDispatcher());
const eventCb = jest.fn();
api.on(ModuleComposerApiEvents.UploaderOptionsChanged, eventCb);
const option = { type: "an_option", label: "New option", onSelected: () => {} };
api.addFileUploadOption(option);
expect(api.fileUploadOptions).toHaveLength(1);
expect(api.fileUploadOptions).toContain(option);
expect(eventCb).toHaveBeenCalledWith(option);
});
it("throws if called with the type 'local'", () => {
const api = new ComposerApi(new MatrixDispatcher());
expect(() => api.addFileUploadOption({ type: "local", label: "New option", onSelected: () => {} })).toThrow(
'Option "local" is reserved',
);
});
it("throws if called with a duplicate type", () => {
const api = new ComposerApi(new MatrixDispatcher());
const option = { type: "an_option", label: "New option", onSelected: () => {} };
api.addFileUploadOption(option);
expect(() => api.addFileUploadOption({ ...option })).toThrow('Option "an_option" already exists');
});
});
});