mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-11 01:40:42 +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,
|
||||
HttpApiEvent,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
type RoomType,
|
||||
SyncState,
|
||||
type SyncStateData,
|
||||
@@ -24,9 +25,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { throttle } from "lodash";
|
||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
// what-input helps improve keyboard accessibility
|
||||
import "what-input";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
@@ -50,6 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||
@@ -94,7 +96,6 @@ import VerificationRequestToast from "../views/toasts/VerificationRequestToast";
|
||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
import SoftLogout from "./auth/SoftLogout";
|
||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { copyPlaintext } from "../../utils/strings";
|
||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||
import { initSentry } from "../../sentry";
|
||||
@@ -123,7 +124,7 @@ import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSet
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||
import { Linkify } from "../../HtmlUtils";
|
||||
import { getHtmlText, Linkify } from "../../HtmlUtils";
|
||||
import { NotificationLevel } from "../../stores/notifications/NotificationLevel";
|
||||
import { type UserTab } from "../views/dialogs/UserTab";
|
||||
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
|
||||
@@ -135,6 +136,10 @@ import { LoginSplashView } from "./auth/LoginSplashView";
|
||||
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
||||
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
|
||||
export { default as Views } from "../../Views";
|
||||
@@ -779,6 +784,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
case Action.ViewHomePage:
|
||||
this.viewHome(payload.justRegistered);
|
||||
break;
|
||||
case Action.Share:
|
||||
this.viewShare(payload.format, payload.msg);
|
||||
break;
|
||||
case Action.ViewStartChatOrReuse:
|
||||
this.chatCreateOrReuse(payload.user_id);
|
||||
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> {
|
||||
const modal = Modal.createDialog(CreateRoomDialog, {
|
||||
type,
|
||||
@@ -1739,6 +1799,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
dis.dispatch({
|
||||
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") {
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
} else if (screen === "welcome") {
|
||||
|
||||
@@ -26,6 +26,11 @@ export enum Action {
|
||||
*/
|
||||
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.
|
||||
* 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 Modal from "../../../../src/Modal.tsx";
|
||||
import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts";
|
||||
import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts";
|
||||
import { clearStorage } from "../../../../src/Lifecycle";
|
||||
|
||||
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", () => {
|
||||
@@ -974,6 +1077,22 @@ describe("<MatrixChat />", () => {
|
||||
} 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", () => {
|
||||
|
||||
Reference in New Issue
Block a user