Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into export-conversations
commit
ef944a59f5
|
@ -72,7 +72,7 @@ limitations under the License.
|
|||
|
||||
.mx_AccessibleButton_kind_danger_outline {
|
||||
color: $button-danger-bg-color;
|
||||
background-color: $button-secondary-bg-color;
|
||||
background-color: transparent;
|
||||
border: 1px solid $button-danger-bg-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
|||
|
||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojibase's so will give false
|
||||
|
@ -176,18 +178,31 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
|||
return { tagName, attribs };
|
||||
},
|
||||
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
let src = attribs.src;
|
||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||
// we don't want to allow images with `https?` `src`s.
|
||||
// We also drop inline images (as if they were not present at all) when the "show
|
||||
// images" preference is disabled. Future work might expose some UI to reveal them
|
||||
// like standalone image events have.
|
||||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||
if (!src || !SettingsStore.getValue("showImages")) {
|
||||
return { tagName, attribs: {} };
|
||||
}
|
||||
|
||||
if (!src.startsWith("mxc://")) {
|
||||
const match = MEDIA_API_MXC_REGEX.exec(src);
|
||||
if (match) {
|
||||
src = `mxc://${match[1]}/${match[2]}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.startsWith("mxc://")) {
|
||||
return { tagName, attribs: {} };
|
||||
}
|
||||
|
||||
const width = Number(attribs.width) || 800;
|
||||
const height = Number(attribs.height) || 600;
|
||||
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
|
||||
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
|
||||
return { tagName, attribs };
|
||||
},
|
||||
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
|
|
|
@ -238,6 +238,7 @@ export default class AppTile extends React.Component {
|
|||
case 'm.sticker':
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||
dis.dispatch({ action: 'stickerpicker_close' });
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
|
|
|
@ -244,7 +244,11 @@ export default class TextualBody extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private highlightCode(code: HTMLElement): void {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
// Auto-detect language only if enabled and only for codeblocks
|
||||
if (
|
||||
SettingsStore.getValue("enableSyntaxHighlightLanguageDetection") &&
|
||||
code.parentElement instanceof HTMLPreElement
|
||||
) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
|
|
|
@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
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
|
||||
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]);
|
||||
}, [onHeightChanged, expanded, previews]);
|
||||
|
||||
const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS);
|
||||
const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
|
||||
|
||||
let toggleButton;
|
||||
if (links.length > 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: links.length - shownLinks.length }) }
|
||||
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_LinkPreviewGroup">
|
||||
{ shownLinks.map((link, i) => (
|
||||
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
|
||||
{ showPreviews.map(([link, preview], i) => (
|
||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||
{ i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
|
|
|
@ -21,7 +21,6 @@ import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
|||
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -29,37 +28,15 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
|||
import ImageView from '../elements/ImageView';
|
||||
|
||||
interface IProps {
|
||||
link: string; // the URL being previewed
|
||||
link: string;
|
||||
preview: IPreviewUrlResponse;
|
||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||
onHeightChanged(): void; // called when the preview's contents has loaded
|
||||
}
|
||||
|
||||
interface IState {
|
||||
preview?: IPreviewUrlResponse;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||
export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||
private readonly description = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
preview: null,
|
||||
};
|
||||
|
||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({ preview }, this.props.onHeightChanged);
|
||||
}, (error) => {
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.description.current) {
|
||||
linkifyElement(this.description.current);
|
||||
|
@ -72,12 +49,8 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onImageClick = ev => {
|
||||
const p = this.state.preview;
|
||||
const p = this.props.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
|
||||
|
@ -99,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const p = this.state.preview;
|
||||
const p = this.props.preview;
|
||||
if (!p || Object.keys(p).length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import { RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS } from "./RightPanelStoreP
|
|||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { Action } from '../dispatcher/actions';
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import RoomViewStore from './RoomViewStore';
|
||||
|
||||
interface RightPanelStoreState {
|
||||
// Whether or not to show the right panel at all. We split out rooms and groups
|
||||
|
@ -68,6 +67,7 @@ const MEMBER_INFO_PHASES = [
|
|||
export default class RightPanelStore extends Store<ActionPayload> {
|
||||
private static instance: RightPanelStore;
|
||||
private state: RightPanelStoreState;
|
||||
private lastRoomId: string;
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
@ -147,8 +147,10 @@ export default class RightPanelStore extends Store<ActionPayload> {
|
|||
__onDispatch(payload: ActionPayload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
|
||||
// fallthrough
|
||||
case 'view_group':
|
||||
if (payload.room_id === RoomViewStore.getRoomId()) break; // skip this transition, probably a permalink
|
||||
this.lastRoomId = payload.room_id;
|
||||
|
||||
// Reset to the member list if we're viewing member info
|
||||
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {
|
||||
|
|
|
@ -22,8 +22,10 @@ import sdk from "../../../skinned-sdk";
|
|||
import { mkEvent, mkStubRoom } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import * as languageHandler from "../../../../src/languageHandler";
|
||||
import * as TestUtils from "../../../test-utils";
|
||||
|
||||
const TextualBody = sdk.getComponent("views.messages.TextualBody");
|
||||
const _TextualBody = sdk.getComponent("views.messages.TextualBody");
|
||||
const TextualBody = TestUtils.wrapInMatrixClientContext(_TextualBody);
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
|
@ -305,10 +307,9 @@ describe("<TextualBody />", () => {
|
|||
const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />);
|
||||
expect(wrapper.text()).toBe(ev.getContent().body);
|
||||
|
||||
let widgets = wrapper.find("LinkPreviewWidget");
|
||||
// at this point we should have exactly one widget
|
||||
expect(widgets.length).toBe(1);
|
||||
expect(widgets.at(0).prop("link")).toBe("https://matrix.org/");
|
||||
let widgets = wrapper.find("LinkPreviewGroup");
|
||||
// at this point we should have exactly one link
|
||||
expect(widgets.at(0).prop("links")).toEqual(["https://matrix.org/"]);
|
||||
|
||||
// simulate an event edit and check the transition from the old URL preview to the new one
|
||||
const ev2 = mkEvent({
|
||||
|
@ -333,11 +334,9 @@ describe("<TextualBody />", () => {
|
|||
|
||||
// XXX: this is to give TextualBody enough time for state to settle
|
||||
wrapper.setState({}, () => {
|
||||
widgets = wrapper.find("LinkPreviewWidget");
|
||||
// at this point we should have exactly two widgets (not the matrix.org one anymore)
|
||||
expect(widgets.length).toBe(2);
|
||||
expect(widgets.at(0).prop("link")).toBe("https://vector.im/");
|
||||
expect(widgets.at(1).prop("link")).toBe("https://riot.im/");
|
||||
widgets = wrapper.find("LinkPreviewGroup");
|
||||
// at this point we should have exactly two links (not the matrix.org one anymore)
|
||||
expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue