mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-17 02:10:38 +00:00
Compare commits
13 Commits
t3chguy/mo
...
matthew/sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d12136b7 | ||
|
|
e121377bcf | ||
|
|
5b9944474c | ||
|
|
2f72104302 | ||
|
|
a713b5c9b6 | ||
|
|
a597cd0d6f | ||
|
|
1f9ee07b02 | ||
|
|
46dbb8159d | ||
|
|
0bee6ef187 | ||
|
|
0bee060dbc | ||
|
|
302baa061f | ||
|
|
21e79710c6 | ||
|
|
0948475192 |
34
playwright/e2e/share-dialog/share-by-url.spec.ts
Normal file
34
playwright/e2e/share-dialog/share-by-url.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("share from URL", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
room: async ({ app }, use) => {
|
||||||
|
const roomId = await app.client.createRoom({ name: "A test room" });
|
||||||
|
await use({ roomId });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should share message when users navigates to share URL", async ({ page, user, room, app }) => {
|
||||||
|
await page.goto("/#/share?msg=Hello+world");
|
||||||
|
// The forward message dialog doesn't update as new infomation arrives via sync, which means sometimes
|
||||||
|
// this is just says, "Empty room". For the same reason, we can't reliably write a test for loading the
|
||||||
|
// app straight away with a /#/share url as the room doesn't appear until the client syncs.]
|
||||||
|
// Ideally we should fix the forward dialog to update and eliminate races, until then, there is only one
|
||||||
|
// room so we click the first button.
|
||||||
|
await page.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await app.viewRoomByName("A test room");
|
||||||
|
const lastMessage = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
||||||
|
await expect(lastMessage).toBeVisible();
|
||||||
|
const lastMessageText = await lastMessage.locator(".mx_EventTile_body").innerText();
|
||||||
|
await expect(lastMessageText).toBe("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
HttpApiEvent,
|
HttpApiEvent,
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
type MatrixEvent,
|
MatrixEvent,
|
||||||
|
MsgType,
|
||||||
type RoomType,
|
type RoomType,
|
||||||
SyncState,
|
SyncState,
|
||||||
type SyncStateData,
|
type SyncStateData,
|
||||||
@@ -24,9 +25,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
// what-input helps improve keyboard accessibility
|
// what-input helps improve keyboard accessibility
|
||||||
import "what-input";
|
import "what-input";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||||
@@ -50,6 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
|
|||||||
import { startAnyRegistrationFlow } from "../../Registration";
|
import { startAnyRegistrationFlow } from "../../Registration";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||||
|
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||||
@@ -94,7 +96,6 @@ import VerificationRequestToast from "../views/toasts/VerificationRequestToast";
|
|||||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
import SoftLogout from "./auth/SoftLogout";
|
import SoftLogout from "./auth/SoftLogout";
|
||||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
|
||||||
import { copyPlaintext } from "../../utils/strings";
|
import { copyPlaintext } from "../../utils/strings";
|
||||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||||
import { initSentry } from "../../sentry";
|
import { initSentry } from "../../sentry";
|
||||||
@@ -123,7 +124,7 @@ import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSet
|
|||||||
import GenericToast from "../views/toasts/GenericToast";
|
import GenericToast from "../views/toasts/GenericToast";
|
||||||
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
||||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||||
import { Linkify } from "../../HtmlUtils";
|
import { getHtmlText, Linkify } from "../../HtmlUtils";
|
||||||
import { NotificationLevel } from "../../stores/notifications/NotificationLevel";
|
import { NotificationLevel } from "../../stores/notifications/NotificationLevel";
|
||||||
import { type UserTab } from "../views/dialogs/UserTab";
|
import { type UserTab } from "../views/dialogs/UserTab";
|
||||||
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
|
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
|
||||||
@@ -135,6 +136,10 @@ import { LoginSplashView } from "./auth/LoginSplashView";
|
|||||||
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||||
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
||||||
import { setTheme } from "../../theme";
|
import { setTheme } from "../../theme";
|
||||||
|
import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenForwardDialogPayload";
|
||||||
|
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
|
||||||
|
import Markdown from "../../Markdown";
|
||||||
|
import { sanitizeHtmlParams } from "../../Linkify";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
@@ -779,6 +784,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
case Action.ViewHomePage:
|
case Action.ViewHomePage:
|
||||||
this.viewHome(payload.justRegistered);
|
this.viewHome(payload.justRegistered);
|
||||||
break;
|
break;
|
||||||
|
case Action.Share:
|
||||||
|
this.viewShare(payload.format, payload.msg);
|
||||||
|
break;
|
||||||
case Action.ViewStartChatOrReuse:
|
case Action.ViewStartChatOrReuse:
|
||||||
this.chatCreateOrReuse(payload.user_id);
|
this.chatCreateOrReuse(payload.user_id);
|
||||||
break;
|
break;
|
||||||
@@ -1114,6 +1122,58 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private viewShare(format: ShareFormat, msg: string): void {
|
||||||
|
// Wait for the first sync so we can present possible rooms to share into
|
||||||
|
this.firstSyncPromise.promise.then(() => {
|
||||||
|
this.notifyNewScreen("share");
|
||||||
|
let rawEvent;
|
||||||
|
switch (format) {
|
||||||
|
case ShareFormat.Html: {
|
||||||
|
rawEvent = {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: getHtmlText(msg),
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: sanitizeHtml(msg, sanitizeHtmlParams),
|
||||||
|
},
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ShareFormat.Markdown: {
|
||||||
|
const html = new Markdown(msg).toHTML({ externalLinks: true });
|
||||||
|
rawEvent = {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: msg,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: html,
|
||||||
|
},
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
rawEvent = {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: msg,
|
||||||
|
},
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const event = new MatrixEvent(rawEvent);
|
||||||
|
dis.dispatch<OpenForwardDialogPayload>({
|
||||||
|
action: Action.OpenForwardDialog,
|
||||||
|
event: event,
|
||||||
|
permalinkCreator: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise<void> {
|
private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise<void> {
|
||||||
const modal = Modal.createDialog(CreateRoomDialog, {
|
const modal = Modal.createDialog(CreateRoomDialog, {
|
||||||
type,
|
type,
|
||||||
@@ -1739,6 +1799,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.CreateChat,
|
action: Action.CreateChat,
|
||||||
});
|
});
|
||||||
|
} else if (screen === "share") {
|
||||||
|
if (params && params["msg"] !== undefined) {
|
||||||
|
dis.dispatch<SharePayload>({
|
||||||
|
action: Action.Share,
|
||||||
|
msg: params["msg"],
|
||||||
|
format: params["format"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// if we weren't already coming at this from an existing screen
|
||||||
|
// and we're logged in, then explicitly default to home.
|
||||||
|
// if we're not logged in, then the login flow will do the right thing.
|
||||||
|
if (!this.state.currentRoomId && !this.state.currentUserId) {
|
||||||
|
this.viewHome();
|
||||||
|
}
|
||||||
} else if (screen === "settings") {
|
} else if (screen === "settings") {
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
} else if (screen === "welcome") {
|
} else if (screen === "welcome") {
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export enum Action {
|
|||||||
*/
|
*/
|
||||||
ViewUser = "view_user",
|
ViewUser = "view_user",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share a text message by forwarding it to a room selected by the user
|
||||||
|
*/
|
||||||
|
Share = "share",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the user settings. No additional payload information required.
|
* Open the user settings. No additional payload information required.
|
||||||
* Optionally can include an OpenToTabPayload.
|
* Optionally can include an OpenToTabPayload.
|
||||||
|
|||||||
29
src/dispatcher/payloads/SharePayload.ts
Normal file
29
src/dispatcher/payloads/SharePayload.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type ActionPayload } from "../payloads";
|
||||||
|
import { type Action } from "../actions";
|
||||||
|
|
||||||
|
export enum ShareFormat {
|
||||||
|
Text = "text",
|
||||||
|
Html = "html",
|
||||||
|
Markdown = "md",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePayload extends ActionPayload {
|
||||||
|
action: Action.Share;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format of message to be shared (optional)
|
||||||
|
*/
|
||||||
|
format: ShareFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message to be shared.
|
||||||
|
*/
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils";
|
|||||||
import { type ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
import { type ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
||||||
import Modal from "../../../../src/Modal.tsx";
|
import Modal from "../../../../src/Modal.tsx";
|
||||||
import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts";
|
import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts";
|
||||||
|
import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts";
|
||||||
import { clearStorage } from "../../../../src/Lifecycle";
|
import { clearStorage } from "../../../../src/Lifecycle";
|
||||||
|
|
||||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||||
@@ -783,6 +784,108 @@ describe("<MatrixChat />", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should open forward dialog when text message shared", async () => {
|
||||||
|
await getComponentAndWaitForReady();
|
||||||
|
defaultDispatcher.dispatch({ action: Action.Share, format: ShareFormat.Text, msg: "Hello world" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.OpenForwardDialog,
|
||||||
|
event: expect.any(MatrixEvent),
|
||||||
|
permalinkCreator: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find(
|
||||||
|
([call]) => call.action === Action.OpenForwardDialog,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = forwardCall?.[0];
|
||||||
|
|
||||||
|
expect(payload!.event.getContent()).toEqual({
|
||||||
|
msgtype: MatrixJs.MsgType.Text,
|
||||||
|
body: "Hello world",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open forward dialog when html message shared", async () => {
|
||||||
|
await getComponentAndWaitForReady();
|
||||||
|
defaultDispatcher.dispatch({ action: Action.Share, format: ShareFormat.Html, msg: "Hello world" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.OpenForwardDialog,
|
||||||
|
event: expect.any(MatrixEvent),
|
||||||
|
permalinkCreator: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find(
|
||||||
|
([call]) => call.action === Action.OpenForwardDialog,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = forwardCall?.[0];
|
||||||
|
|
||||||
|
expect(payload!.event.getContent()).toEqual({
|
||||||
|
msgtype: MatrixJs.MsgType.Text,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
body: expect.stringContaining("Hello world"),
|
||||||
|
formatted_body: expect.stringContaining("Hello world"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open forward dialog when markdown message shared", async () => {
|
||||||
|
await getComponentAndWaitForReady();
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.Share,
|
||||||
|
format: ShareFormat.Markdown,
|
||||||
|
msg: "Hello *world*",
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.OpenForwardDialog,
|
||||||
|
event: expect.any(MatrixEvent),
|
||||||
|
permalinkCreator: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find(
|
||||||
|
([call]) => call.action === Action.OpenForwardDialog,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = forwardCall?.[0];
|
||||||
|
|
||||||
|
expect(payload!.event.getContent()).toEqual({
|
||||||
|
msgtype: MatrixJs.MsgType.Text,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
body: "Hello *world*",
|
||||||
|
formatted_body: "Hello <em>world</em>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip malicious tags from shared html message", async () => {
|
||||||
|
await getComponentAndWaitForReady();
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.Share,
|
||||||
|
format: ShareFormat.Html,
|
||||||
|
msg: `evil<script src="http://evil.dummy/bad.js" />`,
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.OpenForwardDialog,
|
||||||
|
event: expect.any(MatrixEvent),
|
||||||
|
permalinkCreator: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find(
|
||||||
|
([call]) => call.action === Action.OpenForwardDialog,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = forwardCall?.[0];
|
||||||
|
|
||||||
|
expect(payload!.event.getContent()).toEqual({
|
||||||
|
msgtype: MatrixJs.MsgType.Text,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
body: "evil",
|
||||||
|
formatted_body: "evil",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("logout", () => {
|
describe("logout", () => {
|
||||||
@@ -974,6 +1077,22 @@ describe("<MatrixChat />", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("showScreen", () => {
|
||||||
|
it("should show the 'share' screen", async () => {
|
||||||
|
await getComponent({
|
||||||
|
initialScreenAfterLogin: { screen: "share", params: { msg: "Hello", format: ShareFormat.Text } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: "share",
|
||||||
|
msg: "Hello",
|
||||||
|
format: ShareFormat.Text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with a soft-logged-out session", () => {
|
describe("with a soft-logged-out session", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user