Merge branch 'develop' into show-invite-reasons

This commit is contained in:
Robin Townsend 2021-03-31 15:47:26 -04:00
commit 2477258249
540 changed files with 19085 additions and 3868 deletions

View file

@ -20,6 +20,7 @@ import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
title: string;
@ -33,6 +34,7 @@ interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.AccessibleTooltipButton")
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);

View file

@ -20,7 +20,9 @@ import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.ActionButton")
export default class ActionButton extends React.Component {
static propTypes = {
size: PropTypes.string,

View file

@ -20,7 +20,9 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.AddressSelector")
export default class AddressSelector extends React.Component {
static propTypes = {
onSelected: PropTypes.func.isRequired,

View file

@ -19,11 +19,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress.js';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.elements.AddressTile")
export default class AddressTile extends React.Component {
static propTypes = {
address: UserAddressType.isRequired,
@ -46,9 +47,7 @@ export default class AddressTile extends React.Component {
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
if (isMatrixAddress && address.avatarMxc) {
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
address.avatarMxc, 25, 25, 'crop',
));
imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
} else if (address.addressType === 'email') {
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
}

View file

@ -24,7 +24,9 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import WidgetUtils from "../../../utils/WidgetUtils";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.AppPermission")
export default class AppPermission extends React.Component {
static propTypes = {
url: PropTypes.string.isRequired,

View file

@ -38,7 +38,9 @@ import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions
import {MatrixCapabilities} from "matrix-widget-api";
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.AppTile")
export default class AppTile extends React.Component {
constructor(props) {
super(props);
@ -325,9 +327,13 @@ export default class AppTile extends React.Component {
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;";
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;";
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
const appTileBodyStyles = {};
if (this.props.pointerEvents) {
appTileBodyStyles['pointer-events'] = this.props.pointerEvents;
}
const loadingElement = (
<div className="mx_AppLoading_spinner_fadeIn">
@ -338,7 +344,7 @@ export default class AppTile extends React.Component {
// only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppPermission
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
@ -350,20 +356,20 @@ export default class AppTile extends React.Component {
);
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
{ loadingElement }
</div>
);
} else {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass}>
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg="Error - Mixed content" />
</div>
);
} else {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
{ this.state.loading && loadingElement }
<iframe
allow={iframeFeatures}
@ -477,6 +483,8 @@ AppTile.propTypes = {
showPopout: PropTypes.bool,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
// sets the pointer-events property on the iframe
pointerEvents: PropTypes.string,
};
AppTile.defaultProps = {

View file

@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
import BaseDialog from "..//dialogs/BaseDialog"
import AccessibleButton from './AccessibleButton';
import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
import {replaceableComponent} from "../../../utils/replaceableComponent";
export interface DesktopCapturerSource {
id: string;
@ -69,6 +70,7 @@ export interface DesktopCapturerSourcePickerIProps {
onFinished(source: DesktopCapturerSource): void;
}
@replaceableComponent("views.elements.DesktopCapturerSourcePicker")
export default class DesktopCapturerSourcePicker extends React.Component<
DesktopCapturerSourcePickerIProps,
DesktopCapturerSourcePickerIState

View file

@ -19,10 +19,12 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/**
* Basic container for buttons in modal dialogs.
*/
@replaceableComponent("views.elements.DialogButtons")
export default class DialogButtons extends React.Component {
static propTypes = {
// The primary button which is styled differently and has default focus.

View file

@ -18,7 +18,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.DirectorySearchBox")
export default class DirectorySearchBox extends React.Component {
constructor(props) {
super(props);

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
className: string;
@ -33,6 +34,7 @@ export interface ILocationState {
currentY: number;
}
@replaceableComponent("views.elements.Draggable")
export default class Draggable extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -22,6 +22,7 @@ import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
import {Key} from "../../../Keyboard";
import {replaceableComponent} from "../../../utils/replaceableComponent";
class MenuOption extends React.Component {
constructor(props) {
@ -83,6 +84,7 @@ MenuOption.propTypes = {
*
* TODO: Port NetworkDropdown to use this.
*/
@replaceableComponent("views.elements.Dropdown")
export default class Dropdown extends React.Component {
constructor(props) {
super(props);

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
export class EditableItem extends React.Component {
static propTypes = {
@ -85,6 +86,7 @@ export class EditableItem extends React.Component {
}
}
@replaceableComponent("views.elements.EditableItemList")
export default class EditableItemList extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,

View file

@ -18,7 +18,9 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Key} from "../../../Keyboard";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.EditableText")
export default class EditableText extends React.Component {
static propTypes = {
onValueChanged: PropTypes.func,

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/**
* A component which wraps an EditableText, with a spinner while updates take
@ -29,6 +30,7 @@ import * as sdk from '../../../index';
* similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property.
*/
@replaceableComponent("views.elements.EditableTextContainer")
export default class EditableTextContainer extends React.Component {
constructor(props) {
super(props);

View file

@ -21,11 +21,13 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
import {replaceableComponent} from "../../../utils/replaceableComponent";
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
@replaceableComponent("views.elements.ErrorBoundary")
export default class ErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);

View file

@ -23,6 +23,7 @@ import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
import {Layout} from "../../../settings/Layout";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
/**
@ -74,6 +75,7 @@ interface IState {
const AVATAR_SIZE = 32;
@replaceableComponent("views.elements.EventTilePreview")
export default class EventTilePreview extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -97,11 +99,6 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
}
private fakeEvent({message}: IState) {
const avatarUrl = Avatar.avatarUrlForUser(
{ avatarUrl: this.props.avatarUrl },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
// Fake it till we make it
/* eslint-disable quote-props */
const rawEvent = {
@ -112,12 +109,12 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
msgtype: "m.text",
body: message,
displayname: this.props.displayName,
avatar_url: avatarUrl,
avatar_url: this.props.avatarUrl,
},
msgtype: "m.text",
body: message,
displayname: this.props.displayName,
avatar_url: avatarUrl,
avatar_url: this.props.avatarUrl,
},
unsigned: {
age: 97,
@ -133,8 +130,12 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
name: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return avatarUrl;
return Avatar.avatarUrlForUser(
{ avatarUrl: this.props.avatarUrl },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
};
return event;

View file

@ -0,0 +1,66 @@
/*
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, { HTMLAttributes } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers";
const DEFAULT_NUM_FACES = 5;
interface IProps extends HTMLAttributes<HTMLSpanElement> {
room: Room;
onlyKnownUsers?: boolean;
numShown?: number;
}
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
let members = useRoomMembers(room);
// sort users with an explicit avatar first
const iteratees = [member => !!member.getMxcAvatarUrl()];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member));
}
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
return <div {...props} className="mx_FacePile">
<div className="mx_FacePile_faces">
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}>
<MemberAvatar member={member} width={28} height={28} />
</TextWithTooltip>;
}) }
</div>
{ onlyKnownUsers && <span>
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</div>
};
export default FacePile;

View file

@ -14,13 +14,13 @@
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
class FlairAvatar extends React.Component {
@ -40,8 +40,7 @@ class FlairAvatar extends React.Component {
}
render() {
const httpUrl = this.context.mxcUrlToHttp(
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
const tooltip = this.props.groupProfile.name ?
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
this.props.groupProfile.groupId;
@ -64,6 +63,7 @@ FlairAvatar.propTypes = {
FlairAvatar.contextType = MatrixClientContext;
@replaceableComponent("views.elements.Flair")
export default class Flair extends React.Component {
constructor() {
super();

View file

@ -18,6 +18,7 @@ import React from 'react';
import SettingsStore from "../../../settings/SettingsStore";
import Draggable, {ILocationState} from './Draggable';
import { SettingLevel } from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
// Current room
@ -31,6 +32,7 @@ interface IState {
IRCLayoutRoot: HTMLElement;
}
@replaceableComponent("views.elements.IRCTimelineProfileResizer")
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -26,7 +26,9 @@ import Modal from "../../../Modal";
import * as sdk from "../../../index";
import {Key} from "../../../Keyboard";
import FocusLock from "react-focus-lock";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.ImageView")
export default class ImageView extends React.Component {
static propTypes = {
src: PropTypes.string.isRequired, // the source of the image being displayed

View file

@ -20,6 +20,7 @@ import classNames from 'classnames';
import Tooltip from './Tooltip';
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps {
tooltip?: React.ReactNode;
@ -30,6 +31,7 @@ interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.InfoTooltip")
export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from "react";
import {_t} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.InlineSpinner")
export default class InlineSpinner extends React.Component {
render() {
const w = this.props.w || 16;

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from "prop-types";
import ToggleSwitch from "./ToggleSwitch";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.LabelledToggleSwitch")
export default class LabelledToggleSwitch extends React.Component {
static propTypes = {
// The value for the toggle switch

View file

@ -22,6 +22,7 @@ import * as sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@ -29,6 +30,7 @@ function languageMatchesSearchQuery(query, language) {
return false;
}
@replaceableComponent("views.elements.LanguageDropdown")
export default class LanguageDropdown extends React.Component {
constructor(props) {
super(props);
@ -100,10 +102,10 @@ export default class LanguageDropdown extends React.Component {
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null;
if (language) {
value = this.props.value || language;
value = this.props.value || language;
} else {
language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
@ -55,6 +56,7 @@ class ItemRange {
}
}
@replaceableComponent("views.elements.LazyRenderList")
export default class LazyRenderList extends React.Component {
constructor(props) {
super(props);

View file

@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
// An array of member events to summarise
@ -69,6 +70,7 @@ enum TransitionType {
const SEP = ",";
@replaceableComponent("views.elements.MemberEventListSummary")
export default class MemberEventListSummary extends React.Component<IProps> {
static defaultProps = {
summaryLength: 1,

View file

@ -24,6 +24,7 @@ import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -56,6 +57,7 @@ function getOrCreateContainer(containerId) {
* children are made visible and are positioned into a div that is given the same
* bounding rect as the parent of PE.
*/
@replaceableComponent("views.elements.PersistedElement")
export default class PersistedElement extends React.Component {
static propTypes = {
// Unique identifier for this PersistedElement instance

View file

@ -21,7 +21,9 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.PersistentApp")
export default class PersistentApp extends React.Component {
state = {
roomId: RoomViewStore.getRoomId(),
@ -31,6 +33,7 @@ export default class PersistentApp extends React.Component {
componentDidMount() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
}
componentWillUnmount() {
@ -38,6 +41,9 @@ export default class PersistentApp extends React.Component {
this._roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
}
}
_onRoomViewStoreUpdate = payload => {
@ -53,16 +59,28 @@ export default class PersistentApp extends React.Component {
});
};
_onMyMembership = async (room, membership) => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") {
// we're not in the room anymore - delete
if (room.roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
}
}
};
render() {
if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (this.state.roomId !== persistentWidgetInRoomId) {
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
// Sanity check the room - the widget may have been destroyed between render cycles, and
// thus no room is associated anymore.
if (!persistentWidgetInRoom) return null;
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
// Sanity check the room - the widget may have been destroyed between render cycles, and
// thus no room is associated anymore.
if (!persistentWidgetInRoom) return null;
const myMembership = persistentWidgetInRoom.getMyMembership();
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();

View file

@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2019, 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.
@ -19,14 +17,19 @@ import React from 'react';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import {mediaFromMxc} from "../../../customisations/Media";
import Tooltip from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.Pill")
class Pill extends React.Component {
static roomNotifPos(text) {
return text.indexOf("@room");
@ -68,6 +71,8 @@ class Pill extends React.Component {
group: null,
// The room related to the room pill
room: null,
// Is the user hovering the pill
hover: false,
};
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@ -154,6 +159,18 @@ class Pill extends React.Component {
this._unmounted = true;
}
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseLeave = () => {
this.setState({
hover: false,
});
};
doProfileLookup(userId, member) {
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
if (this._unmounted) {
@ -226,7 +243,7 @@ class Pill extends React.Component {
case Pill.TYPE_ROOM_MENTION: {
const room = this.state.room;
if (room) {
linkText = resource;
linkText = room.name || resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
@ -237,12 +254,12 @@ class Pill extends React.Component {
case Pill.TYPE_GROUP_MENTION: {
if (this.state.group) {
const {avatarUrl, groupId, name} = this.state.group;
const cli = MatrixClientPeg.get();
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
avatar = <BaseAvatar
name={name || groupId} width={16} height={16} aria-hidden="true"
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
}
pillClass = 'mx_GroupPill';
}
@ -256,15 +273,36 @@ class Pill extends React.Component {
});
if (this.state.pillType) {
const {yOffset} = this.props;
let tip;
if (this.state.hover && resource) {
tip = <Tooltip label={resource} yOffset={yOffset} />;
}
return <MatrixClientContext.Provider value={this._matrixClient}>
{ this.props.inMessage ?
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
<a
className={classes}
href={href}
onClick={onClick}
data-offset-key={this.props.offsetKey}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{ avatar }
{ linkText }
{ tip }
</a> :
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
<span
className={classes}
data-offset-key={this.props.offsetKey}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{ avatar }
{ linkText }
{ tip }
</span> }
</MatrixClientContext.Provider>;
} else {

View file

@ -20,7 +20,9 @@ import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
import Field from "./Field";
import {Key} from "../../../Keyboard";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.PowerSelector")
export default class PowerSelector extends React.Component {
static propTypes = {
value: PropTypes.number.isRequired,

View file

@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";
@ -31,10 +31,12 @@ import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// 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
// be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component {
static propTypes = {
// the latest event in this chain of replies

View file

@ -19,8 +19,10 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import withValidation from './Validation';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
@replaceableComponent("views.elements.RoomAliasField")
export default class RoomAliasField extends React.PureComponent {
static propTypes = {
domain: PropTypes.string.isRequired,

View file

@ -24,6 +24,7 @@ import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import {mediaFromMxc} from "../../../customisations/Media";
interface ISSOButtonProps extends Omit<IProps, "flow"> {
idp: IIdentityProvider;
@ -72,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24);
icon = <img src={src} height="24" width="24" alt={idp.name} />;
}

View file

@ -21,6 +21,7 @@ import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
// The setting must be a boolean
@ -39,6 +40,7 @@ interface IState {
value: boolean;
}
@replaceableComponent("views.elements.SettingsFlag")
export default class SettingsFlag extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import * as React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
// A callback for the selected value
@ -34,6 +35,7 @@ interface IProps {
disabled: boolean;
}
@replaceableComponent("views.elements.Slider")
export default class Slider extends React.Component<IProps> {
// offset is a terrible inverse approximation.
// if the values represents some function f(x) = y where x is the

View file

@ -0,0 +1,128 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 from 'react';
import Dropdown from "../../views/elements/Dropdown"
import * as sdk from '../../../index';
import PlatformPeg from "../../../PlatformPeg";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
return false;
}
interface SpellCheckLanguagesDropdownIProps {
className: string,
value: string,
onOptionChange(language: string),
}
interface SpellCheckLanguagesDropdownIState {
searchQuery: string,
languages: any,
}
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
SpellCheckLanguagesDropdownIState> {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
languages: null,
};
}
componentDidMount() {
const plaf = PlatformPeg.get();
if (plaf) {
plaf.getAvailableSpellCheckLanguages().then((languages) => {
languages.sort(function(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
const langs = [];
languages.forEach((language) => {
langs.push({
label: language,
value: language,
})
})
this.setState({languages: langs});
}).catch((e) => {
this.setState({languages: ['en']});
});
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
render() {
if (this.state.languages === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />;
}
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.languages.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
});
} else {
displayedLanguages = this.state.languages;
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{ language.label }
</div>;
});
// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propgating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null;
if (language) {
value = this.props.value || language;
} else {
language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}>
{ options }
</Dropdown>;
}
}

View file

@ -15,7 +15,9 @@
*/
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.Spoiler")
export default class Spoiler extends React.Component {
constructor(props) {
super(props);

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
@ -23,6 +24,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
interface IState {
}
@replaceableComponent("views.elements.StyledCheckbox")
export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
private id: string;

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
outlined?: boolean;
@ -24,6 +25,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
interface IState {
}
@replaceableComponent("views.elements.StyledRadioButton")
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {highlightBlock} from 'highlight.js';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.SyntaxHighlight")
export default class SyntaxHighlight extends React.Component {
static propTypes = {
className: PropTypes.string,

View file

@ -30,12 +30,15 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import {mediaFromMxc} from "../../../customisations/Media";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
@replaceableComponent("views.elements.TagTile")
export default class TagTile extends React.Component {
static propTypes = {
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
@ -128,11 +131,11 @@ export default class TagTile extends React.Component {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 32;
const avatarSize = 32;
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const httpUrl = profile.avatarUrl
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
: null;
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
const className = classNames({
@ -178,8 +181,8 @@ export default class TagTile extends React.Component {
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
width={avatarSize}
height={avatarSize}
/>
{contextButton}
{badgeElement}

View file

@ -17,7 +17,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TextWithTooltip")
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
@ -44,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
return (
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children}
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
{children}
{this.state.hover && <Tooltip
label={this.props.tooltip}
tooltipClassName={this.props.tooltipClass}
label={tooltip}
tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> }
</span>
);

View file

@ -18,7 +18,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Tinter from "../../../Tinter";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TintableSvg")
class TintableSvg extends React.Component {
static propTypes = {
src: PropTypes.string.isRequired,

View file

@ -21,6 +21,7 @@ limitations under the License.
import React, {Component, CSSProperties} from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import {replaceableComponent} from "../../../utils/replaceableComponent";
const MIN_TOOLTIP_HEIGHT = 25;
@ -39,6 +40,7 @@ interface IProps {
yOffset?: number;
}
@replaceableComponent("views.elements.Tooltip")
export default class Tooltip extends React.Component<IProps> {
private tooltipContainer: HTMLElement;
private tooltip: void | Element | Component<Element, any, any>;

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TooltipButton")
export default class TooltipButton extends React.Component {
state = {
hover: false,

View file

@ -18,7 +18,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TruncatedList")
export default class TruncatedList extends React.Component {
static propTypes = {
// The number of elements to show before truncating. If negative, no truncation is done.

View file

@ -21,6 +21,7 @@ import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
}
@ -29,6 +30,7 @@ interface IState {
selected: boolean;
}
@replaceableComponent("views.elements.UserTagTile")
export default class UserTagTile extends React.PureComponent<IProps, IState> {
private tagStoreRef: fbEmitter.EventSubscription;