Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/playwright/e2e/messages/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ test.describe("Message url previews", () => {
"og:title": "A simple site",
"og:description": "And with a brief description",
"og:image": mxc,
"og:image:alt": "The riot logo",
},
});
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion apps/web/res/css/_common.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,9 @@ legend {
.mx_AccessSecretStorageDialog button,
.mx_InviteDialog_section button,
.mx_InviteDialog_editor button,
.mx_ModuleOuterDialog button,
[data-kind|="primary"],
[data-kind|="secondary"],
[class|="maplibregl"]
),
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
Expand All @@ -620,11 +623,14 @@ legend {
button:not(
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
[data-kind|="primary"],
[data-kind|="secondary"],
.mx_AccessibleButton,
.mx_UserProfileSettings button,
.mx_ThemeChoicePanel_CustomTheme button,
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
.mx_ModuleOuterDialog button,
.mx_EncryptionUserSettingsTab button
):last-child {
margin-right: 0px;
Expand All @@ -634,6 +640,9 @@ legend {
button:not(
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
[data-kind|="primary"],
[data-kind|="secondary"],
.mx_ModuleOuterDialog button,
.mx_AccessibleButton,
.mx_UserProfileSettings button,
.mx_ThemeChoicePanel_CustomTheme button,
Expand All @@ -659,6 +668,7 @@ legend {
.mx_ThemeChoicePanel_CustomTheme button,
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
.mx_ModuleOuterDialog button,
.mx_EncryptionUserSettingsTab button
),
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
Expand All @@ -678,6 +688,7 @@ legend {
.mx_ThemeChoicePanel_CustomTheme button,
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
.mx_ModuleOuterDialog button,
.mx_EncryptionUserSettingsTab button
),
.mx_Dialog_buttons input[type="submit"].danger {
Expand All @@ -695,7 +706,9 @@ legend {
.mx_Dialog
button:not(
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
[data-kind|="primary"],
[data-kind|="secondary"],
.mx_ModuleOuterDialog button,
.mx_AccessibleButton,
.mx_UserProfileSettings button,
.mx_ThemeChoicePanel_CustomTheme button,
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/PosthogTrackers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { type Interaction as InteractionEvent } from "@matrix-org/analytics-even
import { type PinUnpinAction } from "@matrix-org/analytics-events/types/typescript/PinUnpinAction";
import { type RoomListSortingAlgorithmChanged } from "@matrix-org/analytics-events/types/typescript/RoomListSortingAlgorithmChanged";
import { type UrlPreviewRendered } from "@matrix-org/analytics-events/types/typescript/UrlPreviewRendered";
import { type UrlPreview } from "@element-hq/web-shared-components";

import PageType from "./PageTypes";
import Views from "./Views";
Expand Down Expand Up @@ -151,7 +150,7 @@ export default class PosthogTrackers {
* @param isEncrypted Whether the event (and effectively the room) was encrypted.
* @param previews The previews generated from the event.
*/
public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: UrlPreview[]): void {
public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: { image?: unknown }[]): void {
// Discount any previews that we have already tracked.
if (this.previewedEventIds.get(eventId)) {
return;
Expand Down
35 changes: 34 additions & 1 deletion apps/web/src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ import { RecordingState } from "../../../audio/VoiceRecording";
import type ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from "../../../utils/ShieldUtils";
import SendMessageComposer, { type SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import {
IComposerInsertEventContent,
type ComposerInsertPayload,
} from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";
import type EditorModel from "../../../editor/model";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
Expand All @@ -54,6 +57,7 @@ import { type MatrixClientProps, withMatrixClientHOC } from "../../../contexts/M
import { UIFeature } from "../../../settings/UIFeature";
import { formatTimeLeft } from "../../../DateUtils";
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
import type { ComposerExtraContentPreview } from "@element-hq/element-web-module-api";

// The prefix used when persisting editor drafts to localstorage.
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
Expand Down Expand Up @@ -101,6 +105,7 @@ interface IState {
isWysiwygLabEnabled: boolean;
isRichTextEnabled: boolean;
initialComposerContent: string;
extraEventContent: Map<string, { content: Record<string, unknown>; renderer: ComposerExtraContentPreview }>;
}

type WysiwygComposerState = {
Expand Down Expand Up @@ -152,6 +157,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
isWysiwygLabEnabled: isWysiwygLabEnabled,
isRichTextEnabled: isRichTextEnabled,
initialComposerContent: initialComposerContent,
extraEventContent: new Map(),
};

this.instanceId = instanceCount++;
Expand Down Expand Up @@ -276,6 +282,22 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
break;

case Action.ComposerInsert: {
const composerInsertPayload = payload as IComposerInsertEventContent;
if (
!composerInsertPayload.eventContent ||
!composerInsertPayload.key ||
!composerInsertPayload.previewRenderable
) {
this.setState((s) => {
s.extraEventContent.set(composerInsertPayload.key, {
content: composerInsertPayload.eventContent,
renderer: composerInsertPayload.previewRenderable,
});
});
}
break;
}
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
switch (settingUpdatedPayload.settingName) {
Expand Down Expand Up @@ -526,6 +548,17 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
};

private readonly onExtraContentChange = (key: string, newContent: Record<string, unknown> | null): void => {
this.setState((s) => {
if (newContent === null) {
s.extraEventContent.delete(key);
} else {
s.extraEventContent.set(key, { ...s.extraEventContent.get(key)!, content: newContent });
}
return s;
});
};

public render(): React.ReactNode {
let leftIcon: false | JSX.Element = false;
if (!this.state.isWysiwygLabEnabled) {
Expand Down
64 changes: 42 additions & 22 deletions apps/web/src/components/views/rooms/MessageComposerButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
StickerIcon,
TextFormattingIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { ComposerApiFileUploadLocal } from "@element-hq/element-web-module-api";
import { MultiOptionButton } from "@element-hq/web-shared-components";

import { _t } from "../../../languageHandler";
import { CollapsibleButton } from "./CollapsibleButton";
Expand All @@ -43,6 +45,7 @@ import { filterBoolean } from "../../../utils/arrays";
import { useSettingValue } from "../../../hooks/useSettings";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
import { ModuleApi } from "../../../modules/Api.ts";

interface IProps {
addEmoji: (emoji: string) => boolean;
Expand Down Expand Up @@ -89,7 +92,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
),
];
moreButtons = [
uploadButton(), // props passed via UploadButtonContext
<UploadButton key="uploads" />, // props passed via UploadButtonContext
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton ? pollButton(room, props.relation) : null,
Expand All @@ -106,7 +109,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
) : (
emojiButton(props)
),
uploadButton(), // props passed via UploadButtonContext
<UploadButton key="uploads" />, // props passed via UploadButtonContext
];
moreButtons = [
showStickersButton(props),
Expand Down Expand Up @@ -164,9 +167,43 @@ function emojiButton(props: IProps): ReactElement {
);
}

function uploadButton(): ReactElement {
return <UploadButton key="controls_upload" />;
}
const UploadButton: React.FC = () => {
const overflowMenuCloser = useContext(OverflowMenuContext);
const onLocalUploadClick = useContext(UploadButtonContext);
const { room } = useScopedRoomContext("room");

const onLocalClick = (): void => {
onLocalUploadClick?.();
overflowMenuCloser?.(); // close overflow menu
};

const options = [...ModuleApi.instance.composer.fileUploadOptions.values()].map((uploadOption) => {
if (uploadOption.type === ComposerApiFileUploadLocal) {
return {
icon: AttachmentIcon,
label: _t("common|attachment"),
onSelect: onLocalClick,
};
} else {
return {
icon: uploadOption.icon,
label: uploadOption.label,
onSelect: () => {
uploadOption.onSelected(room!.roomId, (res) => {
console.log("Do something with the result", res);
});
},
};
}
});

return (
<MultiOptionButton
options={options}
multipleOptionsButton={{ label: _t("action|upload_file"), icon: AttachmentIcon }}
/>
);
};

type UploadButtonFn = () => void;
export const UploadButtonContext = createContext<UploadButtonFn | null>(null);
Expand Down Expand Up @@ -234,23 +271,6 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
);
};

// Must be rendered within an UploadButtonContextProvider
const UploadButton: React.FC = () => {
const overflowMenuCloser = useContext(OverflowMenuContext);
const uploadButtonFn = useContext(UploadButtonContext);

const onClick = (): void => {
uploadButtonFn?.();
overflowMenuCloser?.(); // close overflow menu
};

return (
<CollapsibleButton className="mx_MessageComposer_button" onClick={onClick} title={_t("common|attachment")}>
<AttachmentIcon />
</CollapsibleButton>
);
};

function showStickersButton(props: IProps): ReactElement | null {
return props.showStickersButton ? (
<CollapsibleButton
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function createMessageContent(
model: EditorModel,
replyToEvent: MatrixEvent | undefined,
relation: IEventRelation | undefined,
extraEventContent?: Record<string, unknown>,
): RoomMessageEventContent {
const isEmote = containsEmote(model);
if (isEmote) {
Expand All @@ -85,6 +86,7 @@ export function createMessageContent(
const content: RoomMessageEventContent = {
msgtype: isEmote ? MsgType.Emote : MsgType.Text,
body: body,
...extraEventContent,
};
const formattedBody = htmlSerializeIfNeeded(model, {
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
Expand Down Expand Up @@ -130,6 +132,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
disabled?: boolean;
onChange?(model: EditorModel): void;
toggleStickerPickerOpen: () => void;
extraEventContent?: Record<string, unknown>;
}

export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
Expand Down Expand Up @@ -160,6 +163,9 @@ 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 = {
extraEventContent: new Map(),
};
}

public componentDidMount(): void {
Expand Down Expand Up @@ -411,6 +417,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
model,
replyToEvent,
this.props.relation,
this.props.extraEventContent,
);
}
// don't bother sending an empty message
Expand All @@ -428,6 +435,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
(actualRoomId: string) => this.props.mxClient.sendMessage(actualRoomId, threadId ?? null, content!),
this.props.mxClient,
);
// Clear existing links
this.setState({ extraEventContent: new Map() });
if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
Expand Down Expand Up @@ -638,6 +647,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
private focusComposer = (): void => {
this.editorRef.current?.focus();
};
1;

public render(): React.ReactNode {
const threadId =
Expand Down
12 changes: 11 additions & 1 deletion apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { type ActionPayload } from "../payloads";
import { type Action } from "../actions";
import { type TimelineRenderingType } from "../../contexts/RoomContext";
import { ComposerExtraContentPreview } from "@element-hq/element-web-module-api";

export enum ComposerType {
Send = "send",
Expand All @@ -29,4 +30,13 @@ interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload {
text: string;
}

export type ComposerInsertPayload = IComposerInsertMentionPayload | IComposerInsertPlaintextPayload;
export interface IComposerInsertEventContent<T = Record<string, unknown>> extends IBaseComposerInsertPayload {
key: string;
previewRenderable: ComposerExtraContentPreview<T>;
eventContent: T;
}

export type ComposerInsertPayload =
| IComposerInsertMentionPayload
| IComposerInsertPlaintextPayload
| IComposerInsertEventContent;
2 changes: 2 additions & 0 deletions apps/web/src/modules/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { StoresApi } from "./StoresApi.ts";
import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
import { WidgetApi } from "./WidgetApi.ts";
import { CustomisationsApi } from "./customisationsApi.ts";
import { ComposerApi } from "./ComposerApi.ts";

const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
let used = false;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class ModuleApi implements Api {
public readonly rootNode = document.getElementById("matrixchat")!;
public readonly client = new ClientApi();
public readonly stores = new StoresApi();
public readonly composer = new ComposerApi();

public createRoot(element: Element): Root {
return createRoot(element);
Expand Down
Loading
Loading