/share?msg=foo endpoint using forward message dialog (#29874)
* basic implementation of an /share?msg=foo endpoint * SharePayload * add sharing html & md while we're at it * remove whitespace from imports to appease linter * lint * Add unit test * More tests * Test for showScreen * Use one of the typed strings * Test nasty tags stripped out * Add playwright test * Fix flake by not relying on the name being synced as soon as we load --------- Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
@@ -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";
|
||||
@@ -124,7 +125,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";
|
||||
@@ -136,6 +137,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";
|
||||
@@ -780,6 +785,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;
|
||||
@@ -1115,6 +1123,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,
|
||||
@@ -1742,6 +1802,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;
|
||||
}
|
||||
Reference in New Issue
Block a user