Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Šimon Brandner 2021-07-13 13:21:27 +02:00
commit 0bbdfefd0d
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
63 changed files with 1126 additions and 485 deletions

View file

@ -181,7 +181,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
} else {
this.clearStoredEditorState();
dis.dispatch({ action: 'edit_event', event: null });
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
}
event.preventDefault();
break;
@ -200,7 +200,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
private cancelEdit = (): void => {
this.clearStoredEditorState();
dis.dispatch({ action: "edit_event", event: null });
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
};
private get shouldSaveStoredEditorState(): boolean {
@ -375,7 +375,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
// close the event editing and focus composer
dis.dispatch({ action: "edit_event", event: null });
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
};
private cancelPreviousPendingEdit(): void {
@ -452,6 +452,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
} else if (payload.text) {
this.editorRef.current?.insertPlaintext(payload.text);
}
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
this.editorRef.current.focus();
}
};

View file

@ -0,0 +1,90 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useContext, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
import { useStateToggle } from "../../../hooks/useStateToggle";
import LinkPreviewWidget from "./LinkPreviewWidget";
import AccessibleButton from "../elements/AccessibleButton";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
const INITIAL_NUM_PREVIEWS = 2;
interface IProps {
links: string[]; // the URLs to be previewed
mxEvent: MatrixEvent; // the Event associated with the preview
onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
onHeightChanged(): void; // called when the preview's contents has loaded
}
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle();
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(link => {
return cli.getUrlPreview(link, ts).then(preview => [link, preview], error => {
console.error("Failed to get URL preview: " + error);
});
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}, [links, ts], []);
useEffect(() => {
onHeightChanged();
}, [onHeightChanged, expanded, previews]);
const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
let toggleButton: JSX.Element;
if (previews.length > INITIAL_NUM_PREVIEWS) {
toggleButton = <AccessibleButton onClick={toggleExpanded}>
{ expanded
? _t("Collapse")
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
</AccessibleButton>;
}
return <div className="mx_LinkPreviewGroup">
{ showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{ i === 0 ? (
<AccessibleButton
className="mx_LinkPreviewGroup_hide"
onClick={onCancelClick}
aria-label={_t("Close preview")}
>
<img
className="mx_filterFlipColor"
alt=""
role="presentation"
src={require("../../../../res/img/cancel.svg")}
width="18"
height="18"
/>
</AccessibleButton>
): undefined }
</LinkPreviewWidget>
)) }
{ toggleButton }
</div>;
};
export default LinkPreviewGroup;

View file

@ -1,6 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,71 +15,44 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { AllHtmlEntities } from 'html-entities';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
import { linkifyElement } from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import ImageView from '../elements/ImageView';
interface IProps {
link: string;
preview: IPreviewUrlResponse;
mxEvent: MatrixEvent; // the Event associated with the preview
}
@replaceableComponent("views.rooms.LinkPreviewWidget")
export default class LinkPreviewWidget extends React.Component {
static propTypes = {
link: PropTypes.string.isRequired, // the URL being previewed
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
};
constructor(props) {
super(props);
this.state = {
preview: null,
};
this.unmounted = false;
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
if (this.unmounted) {
return;
}
this.setState(
{ preview: res },
this.props.onHeightChanged,
);
}, (error)=>{
console.error("Failed to get URL preview: " + error);
});
this._description = createRef();
}
export default class LinkPreviewWidget extends React.Component<IProps> {
private readonly description = createRef<HTMLDivElement>();
componentDidMount() {
if (this._description.current) {
linkifyElement(this._description.current);
if (this.description.current) {
linkifyElement(this.description.current);
}
}
componentDidUpdate() {
if (this._description.current) {
linkifyElement(this._description.current);
if (this.description.current) {
linkifyElement(this.description.current);
}
}
componentWillUnmount() {
this.unmounted = true;
}
onImageClick = ev => {
const p = this.state.preview;
private onImageClick = ev => {
const p = this.props.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
const ImageView = sdk.getComponent("elements.ImageView");
let src = p["og:image"];
if (src && src.startsWith("mxc://")) {
@ -100,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component {
};
render() {
const p = this.state.preview;
const p = this.props.preview;
if (!p || Object.keys(p).length === 0) {
return <div />;
}
@ -136,21 +108,21 @@ export default class LinkPreviewWidget extends React.Component {
// opaque string. This does not allow any HTML to be injected into the DOM.
const description = AllHtmlEntities.decode(p["og:description"] || "");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_LinkPreviewWidget">
{ img }
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
<div className="mx_LinkPreviewWidget_title">
<a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a>
{ p["og:site_name"] && <span className="mx_LinkPreviewWidget_siteName">
{ (" - " + p["og:site_name"]) }
</span> }
</div>
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
{ description }
</div>
</div>
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
<img className="mx_filterFlipColor" alt="" role="presentation"
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
</AccessibleButton>
{ this.props.children }
</div>
);
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,9 +21,10 @@ import { linkifyElement } from '../../../HtmlUtils';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { getDisplayAliasForAliasSet } from '../../../Rooms';
export function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
}
export const roomShape = PropTypes.shape({

View file

@ -497,7 +497,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
switch (payload.action) {
case 'reply_to_event':
case Action.FocusComposer:
case Action.FocusSendMessageComposer:
this.editorRef.current?.focus();
break;
case "send_composer_insert":

View file

@ -33,7 +33,7 @@ import RecordingPlayback from "../audio_messages/RecordingPlayback";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import MediaDeviceHandler from "../../../MediaDeviceHandler";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
interface IProps {
room: Room;
@ -95,7 +95,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
duration: Math.round(this.state.recorder.durationSeconds * 1000),
// https://github.com/matrix-org/matrix-doc/pull/3246
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
},
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
@ -135,7 +135,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// change between this and recording, but at least we will have tried.
try {
const devices = await MediaDeviceHandler.getDevices();
if (!devices?.['audioInput']?.length) {
if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
title: _t("No microphone found"),
description: <>