Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/c3

This commit is contained in:
Michael Telatynski 2021-07-14 17:52:32 +01:00
commit 4084a11847
67 changed files with 1784 additions and 253 deletions

View file

@ -46,6 +46,7 @@ import DirectorySearchBox from "../views/elements/DirectorySearchBox";
import ScrollPanel from "./ScrollPanel";
import Spinner from "../views/elements/Spinner";
import { ActionPayload } from "../../dispatcher/payloads";
import { getDisplayAliasForAliasSet } from "../../Rooms";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
@ -833,5 +834,5 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) {
return room.canonical_alias || room.aliases?.[0] || "";
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
}

View file

@ -43,6 +43,7 @@ import { useStateToggle } from "../../hooks/useStateToggle";
import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms";
interface IHierarchyProps {
space: Room;
@ -637,5 +638,5 @@ export default SpaceRoomDirectory;
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
}

View file

@ -17,15 +17,18 @@ limitations under the License.
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlaybackWaveform from "./PlaybackWaveform";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "../rooms/EventTile";
import PlaybackWaveform from "./PlaybackWaveform";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
tileShape?: TileShape;
}
interface IState {
@ -50,15 +53,22 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
this.props.playback.prepare();
}
private get isWaveformable(): boolean {
return this.props.tileShape !== TileShape.Notif
&& this.props.tileShape !== TileShape.FileGrid
&& this.props.tileShape !== TileShape.Pinned;
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
return <div className='mx_MediaBody mx_VoiceMessagePrimaryContainer'>
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
return <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} />
<PlaybackWaveform playback={this.props.playback} />
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
</div>;
}
}

View file

@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import BaseDialog from "./BaseDialog";
import EncryptionPanel from "../right_panel/EncryptionPanel";
import { User } from 'matrix-js-sdk';
import { User } from 'matrix-js-sdk/src/models/user';
interface IProps {
verificationRequest: VerificationRequest;

View file

@ -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');
}

View file

@ -452,6 +452,8 @@ export default class ImageView extends React.Component<IProps, IState> {
<div className="mx_ImageView_panel">
{ info }
<div className="mx_ImageView_toolbar">
{ zoomOutButton }
{ zoomInButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")}
@ -462,8 +464,6 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{ zoomOutButton }
{ zoomInButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")}

View file

@ -1,7 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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.
@ -32,6 +31,7 @@ import sanitizeHtml from "sanitize-html";
import { UIFeature } from "../../../settings/UIFeature";
import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "../rooms/EventTile";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -384,7 +384,7 @@ export default class ReplyThread extends React.Component {
{ dateSep }
<EventTile
mxEvent={ev}
tileShape="reply"
tileShape={TileShape.Reply}
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()}

View file

@ -1,5 +1,5 @@
/*
Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@ -24,6 +24,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { TileShape } from "../rooms/EventTile";
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
@ -306,7 +307,7 @@ export default class MFileBody extends React.Component {
// If the attachment is not encrypted then we check whether we
// are being displayed in the room timeline or in a list of
// files in the right hand side of the screen.
if (this.props.tileShape === "file_grid") {
if (this.props.tileShape === TileShape.FileGrid) {
return (
<span className="mx_MFileBody">
{placeholder}

View file

@ -25,9 +25,11 @@ import { mediaFromContent } from "../../../customisations/Media";
import { decryptFile } from "../../../utils/DecryptFile";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { TileShape } from "../rooms/EventTile";
interface IProps {
mxEvent: MatrixEvent;
tileShape?: TileShape;
}
interface IState {
@ -103,7 +105,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
// At this point we should have a playable state
return (
<span className="mx_MVoiceMessageBody">
<RecordingPlayback playback={this.state.playback} />
<RecordingPlayback playback={this.state.playback} tileShape={this.props.tileShape} />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
);

View file

@ -42,7 +42,7 @@ export default class MessageEvent extends React.Component {
onHeightChanged: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
tileShape: PropTypes.string, // TODO: Use TileShape enum
/* the maximum image height to use, if the event is an image */
maxImageHeight: PropTypes.number,

View file

@ -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-

View file

@ -15,12 +15,13 @@ limitations under the License.
*/
import React from "react";
import { Visibility } from "matrix-js-sdk/src/@types/partials";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Visibility } from "matrix-js-sdk/src/@types/partials";
import DirectoryCustomisations from '../../../customisations/Directory';
interface IProps {
roomId: string;
@ -67,10 +68,15 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
render() {
const client = MatrixClientPeg.get();
const enabled = (
DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
this.props.canSetCanonicalAlias
);
return (
<LabelledToggleSwitch value={this.state.isRoomPublished}
onChange={this.onRoomPublishChange}
disabled={!this.props.canSetCanonicalAlias}
disabled={!enabled}
label={_t("Publish this room to the public in %(domain)s's room directory?", {
domain: client.getDomain(),
})}

View file

@ -194,6 +194,7 @@ export enum TileShape {
FileGrid = "file_grid",
Reply = "reply",
ReplyPreview = "reply_preview",
Pinned = "pinned",
}
interface IProps {
@ -902,7 +903,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_12hr: this.props.isTwelveHour,
// Note: we keep the `sending` state class for tests, not for our styles
mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
@ -935,7 +936,7 @@ export default class EventTile extends React.Component<IProps, IState> {
let avatarSize;
let needsSenderProfile;
if (this.props.tileShape === "notif") {
if (this.props.tileShape === TileShape.Notif) {
avatarSize = 24;
needsSenderProfile = true;
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
@ -949,7 +950,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} else if (this.props.layout == Layout.IRC) {
avatarSize = 14;
needsSenderProfile = true;
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
} else if (this.props.continuation && this.props.tileShape !== TileShape.FileGrid) {
// no avatar or sender profile for continuation messages
avatarSize = 0;
needsSenderProfile = false;
@ -979,7 +980,11 @@ export default class EventTile extends React.Component<IProps, IState> {
}
if (needsSenderProfile) {
if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
if (
!this.props.tileShape
|| this.props.tileShape === TileShape.Reply
|| this.props.tileShape === TileShape.ReplyPreview
) {
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}
@ -1065,7 +1070,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
switch (this.props.tileShape) {
case 'notif': {
case TileShape.Notif: {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return React.createElement(this.props.as || "li", {
"className": classes,
@ -1093,11 +1098,12 @@ export default class EventTile extends React.Component<IProps, IState> {
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
/>
</div>,
]);
}
case 'file_grid': {
case TileShape.FileGrid: {
return React.createElement(this.props.as || "li", {
"className": classes,
"aria-live": ariaLive,
@ -1128,10 +1134,10 @@ export default class EventTile extends React.Component<IProps, IState> {
]);
}
case 'reply':
case 'reply_preview': {
case TileShape.Reply:
case TileShape.ReplyPreview: {
let thread;
if (this.props.tileShape === 'reply_preview') {
if (this.props.tileShape === TileShape.ReplyPreview) {
thread = ReplyThread.makeThread(
this.props.mxEvent,
this.props.onHeightChanged,

View file

@ -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"

View file

@ -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 />;
}

View file

@ -29,6 +29,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { TileShape } from "./EventTile";
interface IProps {
room: Room;
@ -87,6 +88,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
tileShape={TileShape.Pinned}
/>
</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.
@ -24,6 +24,7 @@ import PropTypes from "prop-types";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "./EventTile";
function cancelQuoting() {
dis.dispatch({
@ -90,7 +91,7 @@ export default class ReplyPreview extends React.Component {
<div className="mx_ReplyPreview_clear" />
<EventTile
alwaysShowTimestamps={true}
tileShape="reply_preview"
tileShape={TileShape.ReplyPreview}
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}

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

@ -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
});

View file

@ -280,6 +280,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const mutedUsers = [];
Object.keys(userLevels).forEach((user) => {
if (!Number.isInteger(userLevels[user])) { return; }
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(

View file

@ -347,6 +347,29 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const state = client.getRoom(this.props.roomId).currentState;
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
const options = [
{
value: HistoryVisibility.Shared,
label: _t('Members only (since the point in time of selecting this option)'),
},
{
value: HistoryVisibility.Invited,
label: _t('Members only (since they were invited)'),
},
{
value: HistoryVisibility.Joined,
label: _t('Members only (since they joined)'),
},
];
// World readable doesn't make sense for encrypted rooms
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
options.unshift({
value: HistoryVisibility.WorldReadable,
label: _t("Anyone"),
});
}
return (
<div>
<div>
@ -357,28 +380,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
name="historyVis"
value={history}
onChange={this.onHistoryRadioToggle}
definitions={[
{
value: HistoryVisibility.WorldReadable,
disabled: !canChangeHistory,
label: _t("Anyone"),
},
{
value: HistoryVisibility.Shared,
disabled: !canChangeHistory,
label: _t('Members only (since the point in time of selecting this option)'),
},
{
value: HistoryVisibility.Invited,
disabled: !canChangeHistory,
label: _t('Members only (since they were invited)'),
},
{
value: HistoryVisibility.Joined,
disabled: !canChangeHistory,
label: _t('Members only (since they joined)'),
},
]}
disabled={!canChangeHistory}
definitions={options}
/>
</div>
);