[CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-develop
This commit is contained in:
commit
fde32f13a5
190 changed files with 6185 additions and 2225 deletions
|
@ -21,6 +21,7 @@ import * as sdk from '../../../index';
|
|||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const COUNTRIES_BY_ISO2 = {};
|
||||
for (const c of COUNTRIES) {
|
||||
|
@ -130,10 +131,17 @@ export default class CountryDropdown extends React.Component {
|
|||
// values between mounting and the initial value propgating
|
||||
const value = this.props.value || this.state.defaultCountry.iso2;
|
||||
|
||||
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._getShortOption}
|
||||
value={value} searchEnabled={true} disabled={this.props.disabled}
|
||||
return <Dropdown
|
||||
id="mx_CountryDropdown"
|
||||
className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
menuWidth={298}
|
||||
getShortOption={this._getShortOption}
|
||||
value={value}
|
||||
searchEnabled={true}
|
||||
disabled={this.props.disabled}
|
||||
label={_t("Country Dropdown")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
|
|
|
@ -20,10 +20,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
<<<<<<< HEAD
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
=======
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
>>>>>>> develop
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
@ -39,10 +44,16 @@ export default createReactClass({
|
|||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -60,12 +71,12 @@ export default createReactClass({
|
|||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
|
@ -149,7 +160,7 @@ export default createReactClass({
|
|||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -172,7 +183,7 @@ export default createReactClass({
|
|||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
|
@ -180,7 +191,7 @@ export default createReactClass({
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
|
@ -189,21 +200,26 @@ export default createReactClass({
|
|||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasStatus: this.hasStatus,
|
||||
|
|
|
@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
|
|
@ -422,7 +422,7 @@ export default createReactClass({
|
|||
</MenuItem>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
||||
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
||||
quoteButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
|
|
|
@ -27,8 +27,8 @@ export default class StatusMessageContextMenu extends React.Component {
|
|||
user: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: this.comittedStatusMessage,
|
||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import * as sdk from '../../../index';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
_onRemoveClick() {
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import sdk from "../../../index";
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -100,6 +101,12 @@ export class TopLeftMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const helpItem = (
|
||||
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
|
||||
{_t("Help")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const settingsItem = (
|
||||
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||
{_t("Settings")}
|
||||
|
@ -115,11 +122,18 @@ export class TopLeftMenu extends React.Component {
|
|||
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{helpItem}
|
||||
{signInOutItem}
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
openHelp = () => {
|
||||
this.closeMenu();
|
||||
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
};
|
||||
|
||||
viewHomePage() {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
this.closeMenu();
|
||||
|
|
|
@ -32,6 +32,7 @@ import IdentityAuthClient from '../../../IdentityAuthClient';
|
|||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
@ -142,39 +143,41 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
|
||||
|
||||
if (e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
} else if (e.keyCode === 38) { // up arrow
|
||||
} else if (e.key === Key.ARROW_UP) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
||||
} else if (e.keyCode === 40) { // down arrow
|
||||
} else if (e.key === Key.ARROW_DOWN) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this._textinput.current.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
} else if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this._textinput.current.value === '') {
|
||||
if (textInput === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this._addAddressesToList([this._textinput.current.value]);
|
||||
this._addAddressesToList([textInput]);
|
||||
}
|
||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._addAddressesToList([this._textinput.current.value]);
|
||||
this._addAddressesToList([textInput]);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
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.
|
||||
|
@ -17,16 +18,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -83,16 +83,6 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
@ -101,7 +91,7 @@ export default createReactClass({
|
|||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
|
||||
if (this.props.hasCancel && e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
|
@ -121,32 +111,38 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={classNames({
|
||||
[this.props.className]: true,
|
||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||
})}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this._onKeyDown,
|
||||
role: "dialog",
|
||||
["aria-labelledby"]: "mx_BaseDialog_title",
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
["aria-describedby"]: this.props.contentId,
|
||||
}}
|
||||
className={classNames({
|
||||
[this.props.className]: true,
|
||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||
})}
|
||||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</FocusTrap>
|
||||
{ this.props.children }
|
||||
</FocusLock>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,8 +25,8 @@ import Modal from '../../../Modal';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class BugReportDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sendLogs: true,
|
||||
busy: false,
|
||||
|
|
|
@ -173,7 +173,7 @@ export default createReactClass({
|
|||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
773
src/components/views/dialogs/DMInviteDialog.js
Normal file
773
src/components/views/dialogs/DMInviteDialog.js
Normal file
|
@ -0,0 +1,773 @@
|
|||
/*
|
||||
Copyright 2019, 2020 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, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/lib/matrix";
|
||||
import * as humanize from "humanize";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
//
|
||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||
class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
*/
|
||||
get name(): string { throw new Error("Member class not implemented"); }
|
||||
|
||||
/**
|
||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||
* be the 3PID address (email).
|
||||
*/
|
||||
get userId(): string { throw new Error("Member class not implemented"); }
|
||||
|
||||
/**
|
||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||
*/
|
||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||
}
|
||||
|
||||
class DirectoryMember extends Member {
|
||||
_userId: string;
|
||||
_displayName: string;
|
||||
_avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this._displayName = userDirResult.display_name;
|
||||
this._avatarUrl = userDirResult.avatar_url;
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._displayName || this._userId;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
return this._avatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
class ThreepidMember extends Member {
|
||||
_id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
// This is a getter that would be falsey on all other implementations. Until we have
|
||||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
return this._id.includes('@');
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||
};
|
||||
|
||||
_onRemove = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onRemove(this.props.member);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
className='mx_DMInviteDialog_userTile_avatar'
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_DMInviteDialog_userTile'>
|
||||
<span className='mx_DMInviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
<AccessibleButton
|
||||
className='mx_DMInviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
lastActiveTs: PropTypes.number,
|
||||
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
|
||||
highlightWord: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
};
|
||||
|
||||
_onClick = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onToggle(this.props.member);
|
||||
};
|
||||
|
||||
_highlightName(str: string) {
|
||||
if (!this.props.highlightWord) return str;
|
||||
|
||||
// We convert things to lowercase for index searching, but pull substrings from
|
||||
// the submitted text to preserve case. Note: we don't need to htmlEntities the
|
||||
// string because React will safely encode the text for us.
|
||||
const lowerStr = str.toLowerCase();
|
||||
const filterStr = this.props.highlightWord.toLowerCase();
|
||||
|
||||
const result = [];
|
||||
|
||||
let i = 0;
|
||||
let ii;
|
||||
while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
|
||||
// Push any text we missed (first bit/middle of text)
|
||||
if (ii > i) {
|
||||
// Push any text we aren't highlighting (middle of text match, or beginning of text)
|
||||
result.push(<span key={i + 'begin'}>{str.substring(i, ii)}</span>);
|
||||
}
|
||||
|
||||
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
|
||||
|
||||
// Highlight the word the user entered
|
||||
const substr = str.substring(i, filterStr.length + i);
|
||||
result.push(<span className='mx_DMInviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
|
||||
i += substr.length;
|
||||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < (str.length - 1)) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
// TODO: [TravisR] Figure out how to i18n this
|
||||
// `humanize` wants seconds for a timestamp, so divide by 1000
|
||||
const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000);
|
||||
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
|
||||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
let checkmark = null;
|
||||
if (this.props.isSelected) {
|
||||
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
|
||||
}
|
||||
|
||||
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||
// the browser from reloading the image source when the avatar remounts).
|
||||
const stackedAvatar = (
|
||||
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
|
||||
{avatar}
|
||||
{checkmark}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||
{stackedAvatar}
|
||||
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||
{timestamp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class DMInviteDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_debounceTimer: number = null;
|
||||
_editorRef: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
targets: [], // array of Member objects (see interface above)
|
||||
filterText: "",
|
||||
recents: this._buildRecents(),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
};
|
||||
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
const room = rooms[userId];
|
||||
const member = room.getMember(userId);
|
||||
if (!member) continue; // just skip people who don't have memberships for some reason
|
||||
|
||||
const lastEventTs = room.timeline && room.timeline.length
|
||||
? room.timeline[room.timeline.length - 1].getTs()
|
||||
: 0;
|
||||
if (!lastEventTs) continue; // something weird is going on with this room
|
||||
|
||||
recents.push({userId, user: member, lastActive: lastEventTs});
|
||||
}
|
||||
|
||||
// Sort the recents by last active to save us time later
|
||||
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||
|
||||
return recents;
|
||||
}
|
||||
|
||||
_buildSuggestions(): {userId: string, user: RoomMember} {
|
||||
const maxConsideredMembers = 200;
|
||||
const client = MatrixClientPeg.get();
|
||||
const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']];
|
||||
const joinedRooms = client.getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join')
|
||||
.filter(r => r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||
|
||||
// Generates { userId: {member, rooms[]} }
|
||||
const memberRooms = joinedRooms.reduce((members, room) => {
|
||||
const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId));
|
||||
for (const member of joinedMembers) {
|
||||
if (!members[member.userId]) {
|
||||
members[member.userId] = {
|
||||
member: member,
|
||||
// Track the room size of the 'picked' member so we can use the profile of
|
||||
// the smallest room (likely a DM).
|
||||
pickedMemberRoomSize: room.getJoinedMemberCount(),
|
||||
rooms: [],
|
||||
};
|
||||
}
|
||||
|
||||
members[member.userId].rooms.push(room);
|
||||
|
||||
if (room.getJoinedMemberCount() < members[member.userId].pickedMemberRoomSize) {
|
||||
members[member.userId].member = member;
|
||||
members[member.userId].pickedMemberRoomSize = room.getJoinedMemberCount();
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}, {});
|
||||
|
||||
// Generates { userId: {member, numRooms, score} }
|
||||
const memberScores = Object.values(memberRooms).reduce((scores, entry) => {
|
||||
const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0);
|
||||
const maxRange = maxConsideredMembers * entry.rooms.length;
|
||||
scores[entry.member.userId] = {
|
||||
member: entry.member,
|
||||
numRooms: entry.rooms.length,
|
||||
score: Math.max(0, Math.pow(1 - (numMembersTotal / maxRange), 5)),
|
||||
};
|
||||
return scores;
|
||||
}, {});
|
||||
|
||||
const members = Object.values(memberScores);
|
||||
members.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
if (a.numRooms === b.numRooms) {
|
||||
return a.member.userId.localeCompare(b.member.userId);
|
||||
}
|
||||
|
||||
return b.numRooms - a.numRooms;
|
||||
}
|
||||
return b.score - a.score;
|
||||
});
|
||||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
_startDm = () => {
|
||||
this.props.onFinished(this.state.targets.map(t => t.userId));
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
this.props.onFinished([]);
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
const term = e.target.value;
|
||||
this.setState({filterText: term});
|
||||
|
||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||
// results because they might still be vaguely accurate, likewise for races which
|
||||
// could happen here.
|
||||
if (this._debounceTimer) {
|
||||
clearTimeout(this._debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(async () => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
// these results useful. This is a race we want to avoid because we could overwrite
|
||||
// more accurate results.
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
serverResultsMixin: r.results.map(u => ({
|
||||
userId: u.user_id,
|
||||
user: new DirectoryMember(u),
|
||||
})),
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error("Error searching user directory:");
|
||||
console.error(e);
|
||||
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||
});
|
||||
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({tryingIdentityServer: true});
|
||||
return;
|
||||
}
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
term,
|
||||
undefined, // callback
|
||||
token,
|
||||
);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !lookup.mxid) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
this.setState({
|
||||
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
userId: lookup.mxid,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error searching identity server:");
|
||||
console.error(e);
|
||||
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||
}
|
||||
}
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
||||
_showMoreRecents = () => {
|
||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_showMoreSuggestions = () => {
|
||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_toggleMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) targets.splice(idx, 1);
|
||||
else targets.push(member);
|
||||
this.setState({targets});
|
||||
};
|
||||
|
||||
_removeMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
this.setState({targets});
|
||||
}
|
||||
};
|
||||
|
||||
_onPaste = async (e) => {
|
||||
// Prevent the text being pasted into the textarea
|
||||
e.preventDefault();
|
||||
|
||||
// Process it as a list of addresses to add instead
|
||||
const text = e.clipboardData.getData("text");
|
||||
const possibleMembers = [
|
||||
// If we can avoid hitting the profile endpoint, we should.
|
||||
...this.state.recents,
|
||||
...this.state.suggestions,
|
||||
...this.state.serverResultsMixin,
|
||||
...this.state.threepidResultsMixin,
|
||||
];
|
||||
const toAdd = [];
|
||||
const failed = [];
|
||||
const potentialAddresses = text.split(/[\s,]+/);
|
||||
for (const address of potentialAddresses) {
|
||||
const member = possibleMembers.find(m => m.userId === address);
|
||||
if (member) {
|
||||
toAdd.push(member.user);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.indexOf('@') > 0 && Email.looksValid(address)) {
|
||||
toAdd.push(new ThreepidMember(address));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address[0] !== '@') {
|
||||
failed.push(address); // not a user ID
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(address);
|
||||
const displayName = profile ? profile.displayname : null;
|
||||
const avatarUrl = profile ? profile.avatar_url : null;
|
||||
toAdd.push(new DirectoryMember({
|
||||
user_id: address,
|
||||
display_name: displayName,
|
||||
avatar_url: avatarUrl,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Error looking up profile for " + address);
|
||||
console.error(e);
|
||||
failed.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, {
|
||||
title: _t('Failed to find the following users'),
|
||||
description: _t(
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
|
||||
{csvNames: failed.join(", ")},
|
||||
),
|
||||
button: _t('OK'),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||
};
|
||||
|
||||
_onClickInputArea = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onUseDefaultIdentityServerClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||
};
|
||||
|
||||
_onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this._cancel();
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
|
||||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
}
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
const filterBy = this.state.filterText.toLowerCase();
|
||||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
<p>{_t("No results")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
if (showNum === sourceMembers.length - 1) showNum++;
|
||||
|
||||
// .slice() will return an incomplete array but won't error on us if we go too far
|
||||
const toRender = sourceMembers.slice(0, showNum);
|
||||
const hasMore = toRender.length < sourceMembers.length;
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
let showMore = null;
|
||||
if (hasMore) {
|
||||
showMore = (
|
||||
<AccessibleButton onClick={showMoreFn} kind="link">
|
||||
{_t("Show more")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const tiles = toRender.map(r => (
|
||||
<DMRoomTile
|
||||
member={r.user}
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.userId}
|
||||
onToggle={this._toggleMember}
|
||||
highlightWord={this.state.filterText}
|
||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
{tiles}
|
||||
{showMore}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<textarea
|
||||
key={"input"}
|
||||
rows={1}
|
||||
onChange={this._updateFilter}
|
||||
defaultValue={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIdentityServerWarning() {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_DMInviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
title={_t("Direct Messages")}
|
||||
>
|
||||
<div className='mx_DMInviteDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"If you can't find someone, ask them for their username, or share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
)}
|
||||
</p>
|
||||
<div className='mx_DMInviteDialog_addressBar'>
|
||||
{this._renderEditor()}
|
||||
{this._renderIdentityServerWarning()}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this._startDm}
|
||||
className='mx_DMInviteDialog_goButton'
|
||||
>
|
||||
{_t("Go")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ import * as Lifecycle from '../../../Lifecycle';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
|
|
|
@ -97,7 +97,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
const client = MatrixClientPeg.get();
|
||||
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
||||
try {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||
// throws upon cancellation before having started
|
||||
this._verifier = await client.requestVerificationDM(
|
||||
|
|
|
@ -16,23 +16,26 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
<<<<<<< HEAD
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
=======
|
||||
import { Room } from "matrix-js-sdk";
|
||||
|
||||
import sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
>>>>>>> develop
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
class DevtoolsComponent extends React.Component {
|
||||
static contextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
class GenericEditor extends DevtoolsComponent {
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
|
@ -67,12 +70,15 @@ class SendCustomEvent extends GenericEditor {
|
|||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, stateKey, evContent} = Object.assign({
|
||||
|
@ -91,11 +97,11 @@ class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
if (this.state.isStateEvent) {
|
||||
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
} else {
|
||||
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
|
||||
return cli.sendEvent(this.props.room.roomId, this.state.eventType, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,13 +160,16 @@ class SendAccountData extends GenericEditor {
|
|||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
isRoomAccountData: PropTypes.bool,
|
||||
forceMode: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, evContent} = Object.assign({
|
||||
|
@ -177,9 +186,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
|
||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||
}
|
||||
return cli.setAccountData(this.state.eventType, content);
|
||||
}
|
||||
|
@ -234,7 +243,7 @@ class SendAccountData extends GenericEditor {
|
|||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
class FilteredList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
|
@ -247,8 +256,8 @@ class FilteredList extends React.Component {
|
|||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
|
@ -305,19 +314,20 @@ class FilteredList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends DevtoolsComponent {
|
||||
class RoomStateExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
||||
this.roomStateEvents = room.currentState.events;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.roomStateEvents = this.props.room.currentState.events;
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
|
@ -373,7 +383,7 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
|
||||
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
|
@ -442,15 +452,18 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends DevtoolsComponent {
|
||||
class AccountDataExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
|
@ -467,11 +480,10 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
|
||||
getData() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.getRoom(this.context.roomId).accountData;
|
||||
return this.props.room.accountData;
|
||||
}
|
||||
return cli.store.accountData;
|
||||
return this.context.store.accountData;
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
|
@ -505,10 +517,14 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
}} forceMode={true} />;
|
||||
return <SendAccountData
|
||||
room={this.props.room}
|
||||
isRoomAccountData={this.state.isRoomAccountData}
|
||||
onBack={this.onBack}
|
||||
inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
}} forceMode={true} />;
|
||||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
|
@ -553,17 +569,20 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class ServersInRoomList extends DevtoolsComponent {
|
||||
class ServersInRoomList extends React.PureComponent {
|
||||
static getLabel() { return _t('View Servers in Room'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = this.props.room;
|
||||
const servers = new Set();
|
||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||
this.servers = Array.from(servers).map(s =>
|
||||
|
@ -602,19 +621,14 @@ const Entries = [
|
|||
ServersInRoomList,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.Component {
|
||||
static childContextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
// client: PropTypes.instanceOf(MatixClient),
|
||||
};
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
|
@ -627,10 +641,6 @@ export default class DevtoolsDialog extends React.Component {
|
|||
this._unmounted = true;
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return { roomId: this.props.roomId };
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
return () => {
|
||||
this.setState({ mode });
|
||||
|
@ -654,15 +664,17 @@ export default class DevtoolsDialog extends React.Component {
|
|||
let body;
|
||||
|
||||
if (this.state.mode) {
|
||||
body = <div>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} />
|
||||
</div>;
|
||||
body = <MatrixClientContext.Consumer>
|
||||
{(cli) => <React.Fragment>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
|
||||
</React.Fragment>}
|
||||
</MatrixClientContext.Consumer>;
|
||||
} else {
|
||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||
body = <div>
|
||||
body = <React.Fragment>
|
||||
<div>
|
||||
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
|
@ -679,7 +691,7 @@ export default class DevtoolsDialog extends React.Component {
|
|||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
|
|
@ -99,7 +99,7 @@ export default createReactClass({
|
|||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
_onShareClicked: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -94,10 +95,14 @@ export default class LogoutDialog extends React.Component {
|
|||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ export default class ReportEventDialog extends PureComponent {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
|
|
|
@ -24,9 +24,16 @@ import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
|
|||
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
||||
<<<<<<< HEAD
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
=======
|
||||
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
>>>>>>> develop
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -52,6 +59,9 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state");
|
||||
const shouldShowBridgeIcon = featureFlag &&
|
||||
BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0;
|
||||
|
||||
tabs.push(new Tab(
|
||||
_td("General"),
|
||||
|
@ -73,6 +83,15 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
|
||||
if (shouldShowBridgeIcon) {
|
||||
tabs.push(new Tab(
|
||||
_td("Bridge Info"),
|
||||
"mx_RoomSettingsDialog_bridgesIcon",
|
||||
<BridgeSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
|
|
|
@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
|
@ -101,7 +101,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER) {
|
||||
if (ev.key === Key.ENTER) {
|
||||
this.onSubmit();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -15,17 +16,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
<<<<<<< HEAD
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import Modal from '../../../../Modal';
|
||||
|
||||
=======
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import sdk from '../../../../index';
|
||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||
>>>>>>> develop
|
||||
import Modal from '../../../../Modal';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import {Key} from "../../../../Keyboard";
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||
const RESTORE_TYPE_SECRET_STORAGE = 2;
|
||||
|
||||
/*
|
||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||
|
@ -35,6 +42,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
super(props);
|
||||
this.state = {
|
||||
backupInfo: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
restoreError: null,
|
||||
|
@ -73,7 +81,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -148,6 +156,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async _restoreWithSecretStorage() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_SECRET_STORAGE,
|
||||
});
|
||||
try {
|
||||
// `accessSecretStorage` may prompt for storage access as needed.
|
||||
const recoverInfo = await accessSecretStorage(async () => {
|
||||
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo,
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
restoreError: e,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -155,10 +189,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
});
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
});
|
||||
|
||||
// If the backup key is stored, we can proceed directly to restore.
|
||||
if (backupKeyStored) {
|
||||
return this._restoreWithSecretStorage();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadError: null,
|
||||
loading: false,
|
||||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error loading backup status", e);
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import {Key} from '../../../Keyboard';
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
|
@ -40,23 +40,23 @@ export default function AccessibleButton(props) {
|
|||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||
// inconsistencies here
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
if (e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.onKeyUp = function(e) {
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
if (e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
|
|||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {title, ...props} = this.props;
|
||||
const {title, children, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
|
@ -57,6 +57,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
|
|||
/> : <div />;
|
||||
return (
|
||||
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
|
||||
{ children }
|
||||
{ tip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
|
|
@ -22,6 +22,8 @@ import * as sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// XXX: This component is *not* cross-signing aware. Once everything is
|
||||
// cross-signing, this component should just go away.
|
||||
export default createReactClass({
|
||||
displayName: 'DeviceVerifyButtons',
|
||||
|
||||
|
@ -59,7 +61,7 @@ export default createReactClass({
|
|||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.device,
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
onUnverifyClick: function() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -15,11 +16,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
class MenuOption extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -48,9 +50,14 @@ class MenuOption extends React.Component {
|
|||
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||
});
|
||||
|
||||
return <div className={optClasses}
|
||||
return <div
|
||||
id={this.props.id}
|
||||
className={optClasses}
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
role="option"
|
||||
aria-selected={this.props.highlighted}
|
||||
ref={this.props.inputRef}
|
||||
>
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
|
@ -66,6 +73,7 @@ MenuOption.propTypes = {
|
|||
dropdownKey: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
inputRef: PropTypes.any,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -86,8 +94,6 @@ export default class Dropdown extends React.Component {
|
|||
this._onRootClick = this._onRootClick.bind(this);
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||
this._onInputKeyPress = this._onInputKeyPress.bind(this);
|
||||
this._onInputKeyUp = this._onInputKeyUp.bind(this);
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._collectRoot = this._collectRoot.bind(this);
|
||||
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||
|
@ -111,6 +117,7 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._button = createRef();
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this._onDocumentClick, false);
|
||||
|
@ -169,40 +176,49 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onMenuOptionClick(dropdownKey) {
|
||||
_close() {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
this.props.onOptionChange(dropdownKey);
|
||||
}
|
||||
|
||||
_onInputKeyPress(e) {
|
||||
// This needs to be on the keypress event because otherwise
|
||||
// it can't cancel the form submission
|
||||
if (e.key == 'Enter') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
e.preventDefault();
|
||||
// their focus was on the input, its getting unmounted, move it to the button
|
||||
if (this._button.current) {
|
||||
this._button.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_onInputKeyUp(e) {
|
||||
// These keys don't generate keypress events and so needs to
|
||||
// be on keyup
|
||||
if (e.key == 'Escape') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
} else if (e.key == 'ArrowDown') {
|
||||
this.setState({
|
||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||
});
|
||||
} else if (e.key == 'ArrowUp') {
|
||||
this.setState({
|
||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||
});
|
||||
_onMenuOptionClick(dropdownKey) {
|
||||
this._close();
|
||||
this.props.onOptionChange(dropdownKey);
|
||||
}
|
||||
|
||||
_onInputKeyDown = (e) => {
|
||||
let handled = true;
|
||||
|
||||
// These keys don't generate keypress events and so needs to be on keyup
|
||||
switch (e.key) {
|
||||
case Key.ENTER:
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
// fallthrough
|
||||
case Key.ESCAPE:
|
||||
this._close();
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this.setState({
|
||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||
});
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this.setState({
|
||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,20 +266,34 @@ export default class Dropdown extends React.Component {
|
|||
return keys[(index - 1) % keys.length];
|
||||
}
|
||||
|
||||
_scrollIntoView(node) {
|
||||
if (node) {
|
||||
node.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "auto",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
const options = React.Children.map(this.props.children, (child) => {
|
||||
const highlighted = this.state.highlightedOption === child.key;
|
||||
return (
|
||||
<MenuOption key={child.key} dropdownKey={child.key}
|
||||
highlighted={this.state.highlightedOption == child.key}
|
||||
<MenuOption
|
||||
id={`${this.props.id}__${child.key}`}
|
||||
key={child.key}
|
||||
dropdownKey={child.key}
|
||||
highlighted={highlighted}
|
||||
onMouseEnter={this._setHighlightedOption}
|
||||
onClick={this._onMenuOptionClick}
|
||||
inputRef={highlighted ? this._scrollIntoView : undefined}
|
||||
>
|
||||
{ child }
|
||||
</MenuOption>
|
||||
);
|
||||
});
|
||||
if (options.length === 0) {
|
||||
return [<div key="0" className="mx_Dropdown_option">
|
||||
return [<div key="0" className="mx_Dropdown_option" role="option">
|
||||
{ _t("No results") }
|
||||
</div>];
|
||||
}
|
||||
|
@ -279,23 +309,35 @@ export default class Dropdown extends React.Component {
|
|||
let menu;
|
||||
if (this.state.expanded) {
|
||||
if (this.props.searchEnabled) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
currentValue = (
|
||||
<input
|
||||
type="text"
|
||||
className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox}
|
||||
onKeyDown={this._onInputKeyDown}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={`${this.props.id}__${this.state.highlightedOption}`}
|
||||
aria-owns={`${this.props.id}_listbox`}
|
||||
aria-disabled={this.props.disabled}
|
||||
aria-label={this.props.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{ this._getMenuOptions() }
|
||||
</div>;
|
||||
menu = (
|
||||
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
||||
{ this._getMenuOptions() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentValue) {
|
||||
const selectedChild = this.props.getShortOption ?
|
||||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
currentValue = <div className="mx_Dropdown_option">
|
||||
currentValue = <div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
|
||||
{ selectedChild }
|
||||
</div>;
|
||||
}
|
||||
|
@ -311,9 +353,18 @@ export default class Dropdown extends React.Component {
|
|||
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||
// to the input, but overflows below it. The root contains both.
|
||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||
<AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this._onInputClick}>
|
||||
<AccessibleButton
|
||||
className="mx_Dropdown_input mx_no_textinput"
|
||||
onClick={this._onInputClick}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={this.state.expanded}
|
||||
disabled={this.props.disabled}
|
||||
inputRef={this._button}
|
||||
aria-label={this.props.label}
|
||||
aria-describedby={`${this.props.id}_value`}
|
||||
>
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow"></span>
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
|
@ -321,6 +372,7 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
Dropdown.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
|
@ -340,4 +392,6 @@ Dropdown.propTypes = {
|
|||
value: PropTypes.string,
|
||||
// negative for consistency with HTML
|
||||
disabled: PropTypes.bool,
|
||||
// ARIA label
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
|
@ -25,13 +25,13 @@ import * as sdk from '../../../index';
|
|||
* Parent components should supply an 'onSubmit' callback which returns a
|
||||
* promise; a spinner is shown until the promise resolves.
|
||||
*
|
||||
* The parent can also supply a 'getIntialValue' callback, which works in a
|
||||
* The parent can also supply a 'getInitialValue' callback, which works in a
|
||||
* similarly asynchronous way. If this is not provided, the initial value is
|
||||
* taken from the 'initialValue' property.
|
||||
*/
|
||||
export default class EditableTextContainer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
|
|
|
@ -66,10 +66,14 @@ export default class Field extends React.PureComponent {
|
|||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
focused: false,
|
||||
};
|
||||
}
|
||||
|
||||
onFocus = (ev) => {
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
|
@ -88,6 +92,9 @@ export default class Field extends React.PureComponent {
|
|||
};
|
||||
|
||||
onBlur = (ev) => {
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
|
@ -112,7 +119,9 @@ export default class Field extends React.PureComponent {
|
|||
allowEmpty,
|
||||
});
|
||||
|
||||
if (feedback) {
|
||||
// this method is async and so we may have been blurred since the method was called
|
||||
// if we have then hide the feedback as withValidation does
|
||||
if (this.state.focused && feedback) {
|
||||
this.setState({
|
||||
valid,
|
||||
feedback,
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
|
@ -40,7 +40,7 @@ class FlairAvatar extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
const httpUrl = this.context.mxcUrlToHttp(
|
||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
|
@ -62,9 +62,7 @@ FlairAvatar.propTypes = {
|
|||
}),
|
||||
};
|
||||
|
||||
FlairAvatar.contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
FlairAvatar.contextType = MatrixClientContext;
|
||||
|
||||
export default class Flair extends React.Component {
|
||||
constructor() {
|
||||
|
@ -92,7 +90,7 @@ export default class Flair extends React.Component {
|
|||
for (const groupId of groups) {
|
||||
let groupProfile = null;
|
||||
try {
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId);
|
||||
} catch (err) {
|
||||
console.error('Could not get profile for group', groupId, err);
|
||||
}
|
||||
|
@ -134,6 +132,4 @@ Flair.propTypes = {
|
|||
groups: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
Flair.contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
Flair.contextType = MatrixClientContext;
|
||||
|
|
|
@ -22,10 +22,14 @@ import PropTypes from 'prop-types';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
<<<<<<< HEAD
|
||||
import filesize from "filesize";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import * as sdk from "../../../index";
|
||||
=======
|
||||
import {Key} from "../../../Keyboard";
|
||||
>>>>>>> develop
|
||||
|
||||
export default class ImageView extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -60,7 +64,7 @@ export default class ImageView extends React.Component {
|
|||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.keyCode === 27) { // escape
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
|
|
|
@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
|
@ -105,9 +106,14 @@ export default class LanguageDropdown extends React.Component {
|
|||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true} value={value}
|
||||
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>;
|
||||
|
|
|
@ -56,14 +56,20 @@ class ItemRange {
|
|||
}
|
||||
|
||||
export default class LazyRenderList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
||||
const intersectRange = range.expand(props.overflowMargin);
|
||||
const renderRange = range.expand(props.overflowItems);
|
||||
const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize();
|
||||
const listHasChangedSize = !!state.renderRange && renderRange.totalSize() !== state.renderRange.totalSize();
|
||||
// only update render Range if the list has shrunk/grown and we need to adjust padding OR
|
||||
// if the new range + overflowMargin isn't contained by the old anymore
|
||||
if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) {
|
||||
if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) {
|
||||
return {renderRange};
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -20,12 +20,13 @@ import createReactClass from 'create-react-class';
|
|||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
|
@ -66,17 +67,6 @@ const Pill = createReactClass({
|
|||
isSelected: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// ID/alias of the room/user
|
||||
|
@ -127,7 +117,7 @@ const Pill = createReactClass({
|
|||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
const localMember = nextProps.room.getMember(resourceId);
|
||||
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
|
||||
member = localMember;
|
||||
if (!localMember) {
|
||||
member = new RoomMember(null, resourceId);
|
||||
|
@ -271,20 +261,22 @@ const Pill = createReactClass({
|
|||
}
|
||||
|
||||
const classes = classNames("mx_Pill", pillClass, {
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().getUserId(),
|
||||
"mx_UserPill_selected": this.props.isSelected,
|
||||
});
|
||||
|
||||
if (this.state.pillType) {
|
||||
return this.props.inMessage ?
|
||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</a> :
|
||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</span>;
|
||||
return <MatrixClientContext.Provider value={this._matrixClient}>
|
||||
{ this.props.inMessage ?
|
||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</a> :
|
||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</span> }
|
||||
</MatrixClientContext.Provider>;
|
||||
} else {
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
return null;
|
||||
|
|
|
@ -21,10 +21,11 @@ import {_t} from '../../../languageHandler';
|
|||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// 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
|
||||
|
@ -38,12 +39,10 @@ export default class ReplyThread extends React.Component {
|
|||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// The loaded events to be rendered as linear-replies
|
||||
|
@ -187,7 +186,7 @@ export default class ReplyThread extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||
// same event handler as Room.redaction as for both we just do forceUpdate
|
||||
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||
|
@ -259,7 +258,7 @@ export default class ReplyThread extends React.Component {
|
|||
try {
|
||||
// ask the client to fetch the event we want using the context API, only interface to do so is to ask
|
||||
// for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
|
||||
await this.context.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
||||
await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
||||
} catch (e) {
|
||||
// if it fails catch the error and return early, there's no point trying to find the event in this case.
|
||||
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
|
||||
|
@ -300,7 +299,7 @@ export default class ReplyThread extends React.Component {
|
|||
} else if (this.state.loadedEv) {
|
||||
const ev = this.state.loadedEv;
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
const room = this.context.matrixClient.getRoom(ev.getRoomId());
|
||||
const room = this.context.getRoom(ev.getRoomId());
|
||||
header = <blockquote className="mx_ReplyThread">
|
||||
{
|
||||
_t('<a>In reply to</a> <pill>', {}, {
|
||||
|
|
|
@ -20,11 +20,13 @@ import * as sdk from '../../../index';
|
|||
import withValidation from './Validation';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
||||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -53,6 +55,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength} />
|
||||
);
|
||||
}
|
||||
|
@ -61,7 +64,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
if (this.props.onChange) {
|
||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onValidate = async (fieldState) => {
|
||||
const result = await this._validationRules(fieldState);
|
||||
|
|
|
@ -24,8 +24,8 @@ export default class SyntaxHighlight extends React.Component {
|
|||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._ref = this._ref.bind(this);
|
||||
}
|
||||
|
|
|
@ -20,17 +20,21 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
<<<<<<< HEAD
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as sdk from '../../../index';
|
||||
=======
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import dis from '../../../dispatcher';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
|
@ -46,8 +50,8 @@ export default createReactClass({
|
|||
tag: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -56,6 +60,8 @@ export default createReactClass({
|
|||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
// Whether or not the context menu is open
|
||||
menuDisplayed: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -81,7 +87,7 @@ export default createReactClass({
|
|||
_onFlairStoreUpdated() {
|
||||
if (this.unmounted) return;
|
||||
FlairStore.getGroupProfileCached(
|
||||
this.context.matrixClient,
|
||||
this.context,
|
||||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
|
@ -112,12 +118,10 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
console.log("DEBUG onMouseOver");
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
console.log("DEBUG onMouseOut");
|
||||
this.setState({hover: false});
|
||||
},
|
||||
|
||||
|
@ -140,12 +144,11 @@ export default createReactClass({
|
|||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 40;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
|
||||
|
@ -164,9 +167,6 @@ export default createReactClass({
|
|||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
}
|
||||
|
||||
const tip = this.state.hover ?
|
||||
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
||||
|
@ -184,14 +184,9 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.openMenu}
|
||||
label={_t("Options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
@ -200,11 +195,10 @@ export default createReactClass({
|
|||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
</AccessibleTooltipButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -19,46 +19,32 @@ import React from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
|
||||
import {KeyCode} from "../../../Keyboard";
|
||||
import sdk from "../../../index";
|
||||
|
||||
// Controlled Toggle Switch element
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
|
||||
const _onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (disabled) return;
|
||||
|
||||
onChange(!checked);
|
||||
};
|
||||
|
||||
const _onKeyDown = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (disabled) return;
|
||||
|
||||
if (e.keyCode === KeyCode.ENTER || e.keyCode === KeyCode.SPACE) {
|
||||
onChange(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
const classes = classNames({
|
||||
"mx_ToggleSwitch": true,
|
||||
"mx_ToggleSwitch_on": checked,
|
||||
"mx_ToggleSwitch_enabled": !disabled,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
return (
|
||||
<div {...props}
|
||||
<AccessibleButton {...props}
|
||||
className={classes}
|
||||
onClick={_onClick}
|
||||
onKeyDown={_onKeyDown}
|
||||
role="checkbox"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="mx_ToggleSwitch_ball" />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -16,54 +16,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import * as recent from './recent';
|
||||
|
||||
const EMOJIBASE_CATEGORY_IDS = [
|
||||
"people", // smileys
|
||||
"people", // actually people
|
||||
"control", // modifiers and such, not displayed in picker
|
||||
"nature",
|
||||
"foods",
|
||||
"places",
|
||||
"activity",
|
||||
"objects",
|
||||
"symbols",
|
||||
"flags",
|
||||
];
|
||||
|
||||
const DATA_BY_CATEGORY = {
|
||||
"people": [],
|
||||
"nature": [],
|
||||
"foods": [],
|
||||
"places": [],
|
||||
"activity": [],
|
||||
"objects": [],
|
||||
"symbols": [],
|
||||
"flags": [],
|
||||
};
|
||||
const DATA_BY_EMOJI = {};
|
||||
|
||||
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
|
||||
EMOJIBASE.forEach(emoji => {
|
||||
if (emoji.unicode.includes(VARIATION_SELECTOR)) {
|
||||
// Clone data into variation-less version
|
||||
emoji = Object.assign({}, emoji, {
|
||||
unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""),
|
||||
});
|
||||
}
|
||||
DATA_BY_EMOJI[emoji.unicode] = emoji;
|
||||
const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group];
|
||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||
}
|
||||
// This is used as the string to match the query against when filtering emojis.
|
||||
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
|
||||
});
|
||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||
export const EMOJI_HEIGHT = 37;
|
||||
|
@ -89,10 +47,8 @@ class EmojiPicker extends React.Component {
|
|||
viewportHeight: 280,
|
||||
};
|
||||
|
||||
// Convert recent emoji characters to emoji data, removing unknowns.
|
||||
this.recentlyUsed = recent.get()
|
||||
.map(unicode => DATA_BY_EMOJI[unicode])
|
||||
.filter(data => !!data);
|
||||
// Convert recent emoji characters to emoji data, removing unknowns and duplicates
|
||||
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
|
||||
this.memoizedDataByCategory = {
|
||||
recent: this.recentlyUsed,
|
||||
...DATA_BY_CATEGORY,
|
||||
|
|
|
@ -19,15 +19,15 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { findEmojiData } from '../../../HtmlUtils';
|
||||
import {getEmojiFromUnicode} from "../../../emoji";
|
||||
|
||||
// We use the variation-selector Heart in Quick Reactions for some reason
|
||||
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
|
||||
const data = findEmojiData(emoji);
|
||||
const data = getEmojiFromUnicode(emoji);
|
||||
if (!data) {
|
||||
throw new Error(`Emoji ${emoji} doesn't exist in emojibase`);
|
||||
}
|
||||
// Prefer our unicode value for quick reactions (which does not have
|
||||
// variation selectors).
|
||||
// Prefer our unicode value for quick reactions as we sometimes use variation selectors.
|
||||
return Object.assign({}, data, { unicode: emoji });
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ export default createReactClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" />
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
|
|
|
@ -97,7 +97,7 @@ export default createReactClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" />
|
||||
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{_t("A new version of Riot is available.")}
|
||||
</div>
|
||||
|
|
|
@ -19,13 +19,18 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
<<<<<<< HEAD
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as sdk from '../../../index';
|
||||
=======
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import dis from '../../../dispatcher';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// XXX this class copies a lot from RoomTile.js
|
||||
export default createReactClass({
|
||||
|
@ -35,8 +40,8 @@ export default createReactClass({
|
|||
group: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -58,7 +63,7 @@ export default createReactClass({
|
|||
onMouseEnter: function() {
|
||||
const state = {hover: true};
|
||||
// Only allow non-guests to access the context menu
|
||||
if (!this.context.matrixClient.isGuest()) {
|
||||
if (!this.context.isGuest()) {
|
||||
state.badgeHover = true;
|
||||
}
|
||||
this.setState(state);
|
||||
|
@ -118,7 +123,7 @@ export default createReactClass({
|
|||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||
this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
|
||||
this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
|
||||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -27,12 +26,13 @@ import { _t } from '../../../languageHandler';
|
|||
import { GroupMemberType } from '../../../groups';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupMemberInfo',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
|
@ -86,7 +86,7 @@ export default createReactClass({
|
|||
_onKick: function() {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
matrixClient: this.context.matrixClient,
|
||||
matrixClient: this.context,
|
||||
groupMember: this.props.groupMember,
|
||||
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||
title: this.state.isUserInvited ? _t('Disinvite this user from community?')
|
||||
|
@ -96,7 +96,7 @@ export default createReactClass({
|
|||
if (!proceed) return;
|
||||
|
||||
this.setState({removingUser: true});
|
||||
this.context.matrixClient.removeUserFromGroup(
|
||||
this.context.removeUserFromGroup(
|
||||
this.props.groupId, this.props.groupMember.userId,
|
||||
).then(() => {
|
||||
// return to the user list
|
||||
|
@ -172,7 +172,7 @@ export default createReactClass({
|
|||
const avatarUrl = this.props.groupMember.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = (<div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>);
|
||||
|
|
|
@ -19,10 +19,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
<<<<<<< HEAD
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as sdk from '../../../index';
|
||||
=======
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import dis from '../../../dispatcher';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupMemberTile',
|
||||
|
@ -36,8 +41,8 @@ export default createReactClass({
|
|||
return {};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
|
@ -53,7 +58,7 @@ export default createReactClass({
|
|||
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
const name = this.props.member.displayname || this.props.member.userId;
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
const avatarUrl = this.context.mxcUrlToHttp(
|
||||
this.props.member.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ export default createReactClass({
|
|||
return {
|
||||
busy: false,
|
||||
ready: false,
|
||||
isGroupPublicised: null,
|
||||
isGroupPublicised: false, // assume false as <ToggleSwitch /> expects a boolean
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -43,7 +43,7 @@ export default createReactClass({
|
|||
_initGroupStore: function(groupId) {
|
||||
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
|
||||
this.setState({
|
||||
isGroupPublicised: GroupStore.getGroupPublicity(groupId),
|
||||
isGroupPublicised: Boolean(GroupStore.getGroupPublicity(groupId)),
|
||||
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,18 +18,18 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupRoomInfo',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
|
@ -207,7 +207,7 @@ export default createReactClass({
|
|||
const avatarUrl = this.state.groupRoom.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = (<div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>);
|
||||
|
|
|
@ -17,10 +17,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
<<<<<<< HEAD
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import * as sdk from '../../../index';
|
||||
=======
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import dis from '../../../dispatcher';
|
||||
import { GroupRoomType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const GroupRoomTile = createReactClass({
|
||||
displayName: 'GroupRoomTile',
|
||||
|
@ -41,7 +46,7 @@ const GroupRoomTile = createReactClass({
|
|||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
const avatarUrl = this.context.mxcUrlToHttp(
|
||||
this.props.groupRoom.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
@ -66,9 +71,7 @@ const GroupRoomTile = createReactClass({
|
|||
},
|
||||
});
|
||||
|
||||
GroupRoomTile.contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
GroupRoomTile.contextType = MatrixClientContext;
|
||||
|
||||
|
||||
export default GroupRoomTile;
|
||||
|
|
|
@ -17,11 +17,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
function nop() {}
|
||||
|
||||
|
@ -37,8 +37,8 @@ const GroupTile = createReactClass({
|
|||
draggable: PropTypes.bool,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -56,7 +56,7 @@ const GroupTile = createReactClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
|
||||
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst getting cached profile for GroupTile', err);
|
||||
|
@ -80,7 +80,7 @@ const GroupTile = createReactClass({
|
|||
const descElement = this.props.showDescription ?
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
|
||||
|
||||
let avatarElement = (
|
||||
|
|
|
@ -15,17 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
<<<<<<< HEAD
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
=======
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupUserSettings',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -36,7 +40,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.context.matrixClient.getJoinedGroups().then((result) => {
|
||||
this.context.getJoinedGroups().then((result) => {
|
||||
this.setState({groups: result.groups || [], error: null});
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
|
|
|
@ -26,6 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
|
|||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
|
||||
// A cached tinted copy of require("../../../../res/img/download.svg")
|
||||
|
@ -214,10 +215,6 @@ export default createReactClass({
|
|||
tileShape: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts a human readable label for the file attachment to use as
|
||||
* link text.
|
||||
|
@ -360,8 +357,9 @@ export default createReactClass({
|
|||
|
||||
// If the attachment is encryped then put the link inside an iframe.
|
||||
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
|
||||
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
|
||||
renderer_url = this.context.appConfig.cross_origin_renderer_url;
|
||||
const appConfig = SdkConfig.get();
|
||||
if (appConfig && appConfig.cross_origin_renderer_url) {
|
||||
renderer_url = appConfig.cross_origin_renderer_url;
|
||||
}
|
||||
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
|
||||
return (
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -26,6 +25,7 @@ import * as sdk from '../../../index';
|
|||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -39,9 +39,7 @@ export default class MImageBody extends React.Component {
|
|||
maxImageHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -71,7 +69,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
this.context.on('sync', this.onClientSync);
|
||||
}
|
||||
|
||||
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
|
||||
|
@ -174,7 +172,7 @@ export default class MImageBody extends React.Component {
|
|||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
||||
return this.context.mxcUrlToHttp(content.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +196,7 @@ export default class MImageBody extends React.Component {
|
|||
// special case to return clientside sender-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
return this.context.matrixClient.mxcUrlToHttp(
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.info.thumbnail_url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
|
@ -221,7 +219,7 @@ export default class MImageBody extends React.Component {
|
|||
pixelRatio === 1.0 ||
|
||||
(!info || !info.w || !info.h || !info.size)
|
||||
) {
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
|
||||
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
|
||||
} else {
|
||||
// we should only request thumbnails if the image is bigger than 800x600
|
||||
// (or 1600x1200 on retina) otherwise the image in the timeline will just
|
||||
|
@ -242,7 +240,7 @@ export default class MImageBody extends React.Component {
|
|||
// image is too large physically and bytewise to clutter our timeline so
|
||||
// we ask for a thumbnail, despite knowing that it will be max 800x600
|
||||
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
|
||||
return this.context.matrixClient.mxcUrlToHttp(
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
|
@ -251,7 +249,7 @@ export default class MImageBody extends React.Component {
|
|||
// download the original image otherwise, so we can scale it client side
|
||||
// to take pixelRatio into account.
|
||||
// ( no width/height means we want the original image)
|
||||
return this.context.matrixClient.mxcUrlToHttp(
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
);
|
||||
}
|
||||
|
@ -308,7 +306,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
|
||||
if (this.state.decryptedUrl) {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
_onRejectClicked = () => {
|
||||
|
|
|
@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
|
|||
import Modal from '../../../Modal';
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import {RoomContext} from "../../structures/RoomView";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -88,7 +88,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
|
|||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}>
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
@ -117,9 +117,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
onFocusChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
room: RoomContext,
|
||||
};
|
||||
static contextType = RoomContext;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
|
@ -164,12 +162,12 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
let editButton;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
if (this.context.room.canReact) {
|
||||
if (this.context.canReact) {
|
||||
reactButton = (
|
||||
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
|
||||
);
|
||||
}
|
||||
if (this.context.room.canReply) {
|
||||
if (this.context.canReply) {
|
||||
replyButton = <AccessibleButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'SenderProfile',
|
||||
|
@ -31,8 +31,8 @@ export default createReactClass({
|
|||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -47,18 +47,18 @@ export default createReactClass({
|
|||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context.matrixClient, this.props.mxEvent.getSender(),
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
|
||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
onRoomStateEvents(event) {
|
||||
|
@ -71,7 +71,7 @@ export default createReactClass({
|
|||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
|
|
|
@ -98,12 +98,12 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_applyFormatting() {
|
||||
this.activateSpoilers(this._content.current.children);
|
||||
this.activateSpoilers([this._content.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
pillifyLinks([this._content.current], this.props.mxEvent);
|
||||
HtmlUtils.linkifyElement(this._content.current);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
|
@ -167,7 +167,8 @@ export default createReactClass({
|
|||
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview) {
|
||||
let links = this.findLinks(this._content.current.children);
|
||||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||
let links = this.findLinks([this._content.current]);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
|
@ -329,10 +330,6 @@ export default createReactClass({
|
|||
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
|
||||
}
|
||||
},
|
||||
|
||||
getInnerText: () => {
|
||||
return this._content.current.innerText;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -433,6 +430,7 @@ export default createReactClass({
|
|||
disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
stripReplyFallback: stripReply,
|
||||
ref: this._content,
|
||||
});
|
||||
if (this.props.replacingEventId) {
|
||||
body = [body, this._renderEditedMarker()];
|
||||
|
@ -459,15 +457,14 @@ export default createReactClass({
|
|||
|
||||
switch (content.msgtype) {
|
||||
case "m.emote":
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
return (
|
||||
<span ref={this._content} className="mx_MEmoteBody mx_EventTile_content">
|
||||
<span className="mx_MEmoteBody mx_EventTile_content">
|
||||
*
|
||||
<span
|
||||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{ name }
|
||||
{ mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender() }
|
||||
</span>
|
||||
|
||||
{ body }
|
||||
|
@ -476,14 +473,14 @@ export default createReactClass({
|
|||
);
|
||||
case "m.notice":
|
||||
return (
|
||||
<span ref={this._content} className="mx_MNoticeBody mx_EventTile_content">
|
||||
<span className="mx_MNoticeBody mx_EventTile_content">
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
);
|
||||
default: // including "m.text"
|
||||
return (
|
||||
<span ref={this._content} className="mx_MTextBody mx_EventTile_content">
|
||||
<span className="mx_MTextBody mx_EventTile_content">
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useMemo, useState, useEffect} from 'react';
|
||||
import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {Group, RoomMember, User} from 'matrix-js-sdk';
|
||||
|
@ -37,9 +37,9 @@ import MultiInviter from "../../../utils/MultiInviter";
|
|||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -58,21 +58,21 @@ const _disambiguateDevices = (devices) => {
|
|||
}
|
||||
};
|
||||
|
||||
const _getE2EStatus = (devices) => {
|
||||
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
|
||||
return hasUnverifiedDevice ? "warning" : "verified";
|
||||
};
|
||||
|
||||
async function unverifyUser(matrixClient, userId) {
|
||||
const devices = await matrixClient.getStoredDevicesForUser(userId);
|
||||
for (const device of devices) {
|
||||
if (device.isVerified()) {
|
||||
matrixClient.setDeviceVerified(
|
||||
userId, device.deviceId, false,
|
||||
);
|
||||
}
|
||||
const _getE2EStatus = (cli, userId, devices) => {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
|
||||
return hasUnverifiedDevice ? "warning" : "verified";
|
||||
}
|
||||
}
|
||||
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
|
||||
const allDevicesVerified = devices.every(device => {
|
||||
const { deviceId } = device;
|
||||
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
|
||||
});
|
||||
if (allDevicesVerified) {
|
||||
return userVerified ? "verified" : "normal";
|
||||
}
|
||||
return "warning";
|
||||
};
|
||||
|
||||
function openDMForUser(matrixClient, userId) {
|
||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
|
@ -98,14 +98,14 @@ function openDMForUser(matrixClient, userId) {
|
|||
}
|
||||
|
||||
function useIsEncrypted(cli, room) {
|
||||
const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId));
|
||||
const [isEncrypted, setIsEncrypted] = useState(room ? cli.isRoomEncrypted(room.roomId) : undefined);
|
||||
|
||||
const update = useCallback((event) => {
|
||||
if (event.getType() === "m.room.encryption") {
|
||||
setIsEncrypted(cli.isRoomEncrypted(room.roomId));
|
||||
}
|
||||
}, [cli, room]);
|
||||
useEventEmitter(room.currentState, "RoomState.events", update);
|
||||
useEventEmitter(room ? room.currentState : undefined, "RoomState.events", update);
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
|
@ -114,21 +114,24 @@ function verifyDevice(userId, device) {
|
|||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: userId,
|
||||
device: device,
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
|
||||
|
||||
const classes = classNames("mx_UserInfo_device", {
|
||||
mx_UserInfo_device_verified: device.isVerified(),
|
||||
mx_UserInfo_device_unverified: !device.isVerified(),
|
||||
mx_UserInfo_device_verified: deviceTrust.isVerified(),
|
||||
mx_UserInfo_device_unverified: !deviceTrust.isVerified(),
|
||||
});
|
||||
const iconClasses = classNames("mx_E2EIcon", {
|
||||
mx_E2EIcon_verified: device.isVerified(),
|
||||
mx_E2EIcon_warning: !device.isVerified(),
|
||||
mx_E2EIcon_verified: deviceTrust.isVerified(),
|
||||
mx_E2EIcon_warning: !deviceTrust.isVerified(),
|
||||
});
|
||||
|
||||
const onDeviceClick = () => {
|
||||
if (!device.isVerified()) {
|
||||
if (!deviceTrust.isVerified()) {
|
||||
verifyDevice(userId, device);
|
||||
}
|
||||
};
|
||||
|
@ -136,7 +139,7 @@ function DeviceItem({userId, device}) {
|
|||
const deviceName = device.ambiguous ?
|
||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted");
|
||||
const trustedLabel = deviceTrust.isVerified() ? _t("Trusted") : _t("Not trusted");
|
||||
return (<AccessibleButton className={classes} onClick={onDeviceClick}>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
|
@ -146,6 +149,7 @@ function DeviceItem({userId, device}) {
|
|||
|
||||
function DevicesSection({devices, userId, loading}) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
|
||||
|
@ -156,9 +160,21 @@ function DevicesSection({devices, userId, loading}) {
|
|||
if (devices === null) {
|
||||
return _t("Unable to load device list");
|
||||
}
|
||||
const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
|
||||
|
||||
const unverifiedDevices = devices.filter(d => !d.isVerified());
|
||||
const verifiedDevices = devices.filter(d => d.isVerified());
|
||||
const unverifiedDevices = [];
|
||||
const verifiedDevices = [];
|
||||
|
||||
for (let i = 0; i < devices.length; ++i) {
|
||||
const device = devices[i];
|
||||
const deviceTrust = deviceTrusts[i];
|
||||
|
||||
if (deviceTrust.isVerified()) {
|
||||
verifiedDevices.push(device);
|
||||
} else {
|
||||
unverifiedDevices.push(device);
|
||||
}
|
||||
}
|
||||
|
||||
let expandButton;
|
||||
if (verifiedDevices.length) {
|
||||
|
@ -192,7 +208,9 @@ function DevicesSection({devices, userId, loading}) {
|
|||
);
|
||||
}
|
||||
|
||||
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
|
||||
const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let ignoreButton = null;
|
||||
let insertPillButton = null;
|
||||
let inviteUserButton = null;
|
||||
|
@ -302,14 +320,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
let unverifyButton;
|
||||
if (devices && devices.some(device => device.isVerified())) {
|
||||
unverifyButton = (
|
||||
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{ _t('Unverify user') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
|
@ -321,11 +331,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
{ insertPillButton }
|
||||
{ inviteUserButton }
|
||||
{ ignoreButton }
|
||||
{ unverifyButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const _warnSelfDemote = async () => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
@ -393,7 +402,12 @@ const useRoomPowerLevels = (cli, room) => {
|
|||
return powerLevels;
|
||||
};
|
||||
|
||||
const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => {
|
||||
const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// check if user can be kicked/disinvited
|
||||
if (member.membership !== "invite" && member.membership !== "join") return null;
|
||||
|
||||
const onKick = async () => {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
const {finished} = Modal.createTrackedDialog(
|
||||
|
@ -433,9 +447,11 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
|
|||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
|
||||
{ kickLabel }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
};
|
||||
|
||||
const RedactMessagesButton = ({member}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}) => {
|
||||
const onRedactAllMessages = async () => {
|
||||
const {roomId, userId} = member;
|
||||
const room = cli.getRoom(roomId);
|
||||
|
@ -506,9 +522,11 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
|
|||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}>
|
||||
{ _t("Remove recent messages") }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
};
|
||||
|
||||
const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => {
|
||||
const onBanOrUnban = async () => {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
const {finished} = Modal.createTrackedDialog(
|
||||
|
@ -562,207 +580,209 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
|
|||
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
||||
{ label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
};
|
||||
|
||||
const MuteToggleButton = withLegacyMatrixClient(
|
||||
({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => {
|
||||
const isMuted = _isMuted(member, powerLevels);
|
||||
const onMuteToggle = async () => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = member.roomId;
|
||||
const target = member.userId;
|
||||
const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
// Don't show the mute/unmute option if the user is not in the room
|
||||
if (member.membership !== "join") return null;
|
||||
|
||||
const isMuted = _isMuted(member, powerLevels);
|
||||
const onMuteToggle = async () => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = member.roomId;
|
||||
const target = member.userId;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
const powerLevels = powerLevelEvent.getContent();
|
||||
const levelToSend = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||
powerLevels.events_default
|
||||
);
|
||||
let level;
|
||||
if (isMuted) { // unmute
|
||||
level = levelToSend;
|
||||
} else { // mute
|
||||
level = levelToSend - 1;
|
||||
}
|
||||
level = parseInt(level);
|
||||
const powerLevels = powerLevelEvent.getContent();
|
||||
const levelToSend = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||
powerLevels.events_default
|
||||
);
|
||||
let level;
|
||||
if (isMuted) { // unmute
|
||||
level = levelToSend;
|
||||
} else { // mute
|
||||
level = levelToSend - 1;
|
||||
}
|
||||
level = parseInt(level);
|
||||
|
||||
if (!isNaN(level)) {
|
||||
startUpdating();
|
||||
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
console.error("Mute error: " + err);
|
||||
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to mute user"),
|
||||
});
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
if (!isNaN(level)) {
|
||||
startUpdating();
|
||||
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
console.error("Mute error: " + err);
|
||||
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to mute user"),
|
||||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: !isMuted,
|
||||
});
|
||||
|
||||
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
|
||||
return <AccessibleButton className={classes} onClick={onMuteToggle}>
|
||||
{ muteLabel }
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
let kickButton;
|
||||
let banButton;
|
||||
let muteButton;
|
||||
let redactButton;
|
||||
|
||||
const editPowerLevel = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
);
|
||||
|
||||
const me = room.getMember(cli.getUserId());
|
||||
const isMe = me.userId === member.userId;
|
||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.kick) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= powerLevels.redact) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
|
||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= editPowerLevel) {
|
||||
muteButton = (
|
||||
<MuteToggleButton
|
||||
member={member}
|
||||
room={room}
|
||||
powerLevels={powerLevels}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (kickButton || banButton || muteButton || redactButton || children) {
|
||||
return <GenericAdminToolsContainer>
|
||||
{ muteButton }
|
||||
{ kickButton }
|
||||
{ banButton }
|
||||
{ redactButton }
|
||||
{ children }
|
||||
</GenericAdminToolsContainer>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, stopUpdating}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [isPrivileged, setIsPrivileged] = useState(false);
|
||||
const [isInvited, setIsInvited] = useState(false);
|
||||
|
||||
// Listen to group store changes
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
|
||||
const onGroupStoreUpdated = () => {
|
||||
if (unmounted) return;
|
||||
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
|
||||
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
|
||||
(m) => m.userId === groupMember.userId,
|
||||
));
|
||||
};
|
||||
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: !isMuted,
|
||||
});
|
||||
GroupStore.registerListener(groupId, onGroupStoreUpdated);
|
||||
onGroupStoreUpdated();
|
||||
// Handle unmount
|
||||
return () => {
|
||||
unmounted = true;
|
||||
GroupStore.unregisterListener(onGroupStoreUpdated);
|
||||
};
|
||||
}, [groupId, groupMember.userId]);
|
||||
|
||||
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
|
||||
return <AccessibleButton className={classes} onClick={onMuteToggle}>
|
||||
{ muteLabel }
|
||||
</AccessibleButton>;
|
||||
},
|
||||
);
|
||||
if (isPrivileged) {
|
||||
const _onKick = async () => {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
|
||||
matrixClient: cli,
|
||||
groupMember,
|
||||
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||
title: isInvited ? _t('Disinvite this user from community?')
|
||||
: _t('Remove this user from community?'),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const RoomAdminToolsContainer = withLegacyMatrixClient(
|
||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
|
||||
let kickButton;
|
||||
let banButton;
|
||||
let muteButton;
|
||||
let redactButton;
|
||||
const [proceed] = await finished;
|
||||
if (!proceed) return;
|
||||
|
||||
const editPowerLevel = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
startUpdating();
|
||||
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
|
||||
// return to the user list
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
}).catch((e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: isInvited ?
|
||||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
console.log(e);
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
});
|
||||
};
|
||||
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
|
||||
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
const me = room.getMember(cli.getUserId());
|
||||
const isMe = me.userId === member.userId;
|
||||
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
|
||||
// No make/revoke admin API yet
|
||||
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||
giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;*/
|
||||
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.kick) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= powerLevels.redact) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
|
||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (canAffectUser && me.powerLevel >= editPowerLevel) {
|
||||
muteButton = (
|
||||
<MuteToggleButton
|
||||
member={member}
|
||||
room={room}
|
||||
powerLevels={powerLevels}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <GenericAdminToolsContainer>
|
||||
{ kickButton }
|
||||
{ children }
|
||||
</GenericAdminToolsContainer>;
|
||||
}
|
||||
|
||||
if (kickButton || banButton || muteButton || redactButton || children) {
|
||||
return <GenericAdminToolsContainer>
|
||||
{ muteButton }
|
||||
{ kickButton }
|
||||
{ banButton }
|
||||
{ redactButton }
|
||||
{ children }
|
||||
</GenericAdminToolsContainer>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
},
|
||||
);
|
||||
|
||||
const GroupAdminToolsSection = withLegacyMatrixClient(
|
||||
({matrixClient: cli, children, groupId, groupMember, startUpdating, stopUpdating}) => {
|
||||
const [isPrivileged, setIsPrivileged] = useState(false);
|
||||
const [isInvited, setIsInvited] = useState(false);
|
||||
|
||||
// Listen to group store changes
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
|
||||
const onGroupStoreUpdated = () => {
|
||||
if (unmounted) return;
|
||||
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
|
||||
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
|
||||
(m) => m.userId === groupMember.userId,
|
||||
));
|
||||
};
|
||||
|
||||
GroupStore.registerListener(groupId, onGroupStoreUpdated);
|
||||
onGroupStoreUpdated();
|
||||
// Handle unmount
|
||||
return () => {
|
||||
unmounted = true;
|
||||
GroupStore.unregisterListener(onGroupStoreUpdated);
|
||||
};
|
||||
}, [groupId, groupMember.userId]);
|
||||
|
||||
if (isPrivileged) {
|
||||
const _onKick = async () => {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
|
||||
matrixClient: cli,
|
||||
groupMember,
|
||||
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||
title: isInvited ? _t('Disinvite this user from community?')
|
||||
: _t('Remove this user from community?'),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [proceed] = await finished;
|
||||
if (!proceed) return;
|
||||
|
||||
startUpdating();
|
||||
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
|
||||
// return to the user list
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
}).catch((e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: isInvited ?
|
||||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
console.log(e);
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
});
|
||||
};
|
||||
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
|
||||
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
// No make/revoke admin API yet
|
||||
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||
giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;*/
|
||||
|
||||
return <GenericAdminToolsContainer>
|
||||
{ kickButton }
|
||||
{ children }
|
||||
</GenericAdminToolsContainer>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
},
|
||||
);
|
||||
return <div />;
|
||||
};
|
||||
|
||||
const GroupMember = PropTypes.shape({
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
@ -838,7 +858,7 @@ function useRoomPermissions(cli, room, user) {
|
|||
return roomPermissions;
|
||||
}
|
||||
|
||||
const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => {
|
||||
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
if (room && user.roomId) { // is in room
|
||||
if (isEditing) {
|
||||
|
@ -865,9 +885,11 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room
|
|||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
@ -942,7 +964,7 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room,
|
|||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) return;
|
||||
if (!confirmed) return;
|
||||
}
|
||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} finally {
|
||||
|
@ -971,12 +993,15 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room,
|
|||
{buttonOrSpinner}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// cli is injected by withLegacyMatrixClient
|
||||
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
// fetch latest room member if we have a room, so we don't show historical information, falling back to user
|
||||
const member = useMemo(() => room ? (room.getMember(user.userId) || user) : user, [room, user]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
@ -1008,7 +1033,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, user);
|
||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
|
@ -1040,16 +1065,8 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
}, [cli, user.userId]);
|
||||
|
||||
|
||||
const onMemberAvatarKey = e => {
|
||||
if (e.key === "Enter") {
|
||||
onMemberAvatarClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const member = user;
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
||||
|
@ -1060,7 +1077,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}, [cli, user]);
|
||||
}, [cli, member]);
|
||||
|
||||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
@ -1077,11 +1094,11 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
|
||||
let adminToolsContainer;
|
||||
if (room && user.roomId) {
|
||||
if (room && member.roomId) {
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={user}
|
||||
member={member}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
|
@ -1111,20 +1128,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
||||
}
|
||||
|
||||
const displayName = user.name || user.displayname;
|
||||
const displayName = member.name || member.displayname;
|
||||
|
||||
let presenceState;
|
||||
let presenceLastActiveAgo;
|
||||
let presenceCurrentlyActive;
|
||||
let statusMessage;
|
||||
|
||||
if (user instanceof RoomMember && user.user) {
|
||||
presenceState = user.user.presence;
|
||||
presenceLastActiveAgo = user.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = user.user.currentlyActive;
|
||||
if (member instanceof RoomMember && member.user) {
|
||||
presenceState = member.user.presence;
|
||||
presenceLastActiveAgo = member.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = member.user.currentlyActive;
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
statusMessage = user.user._unstable_statusMessage;
|
||||
statusMessage = member.user._unstable_statusMessage;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1147,21 +1164,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
||||
}
|
||||
|
||||
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div
|
||||
className="mx_UserInfo_avatar"
|
||||
onClick={onMemberAvatarClick}
|
||||
onKeyDown={onMemberAvatarKey}
|
||||
tabIndex="0"
|
||||
role="img"
|
||||
aria-label={_t("Profile picture")}
|
||||
>
|
||||
<div><div style={{backgroundImage: `url(${httpUrl})`}} /></div>
|
||||
</div>;
|
||||
}
|
||||
// const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const avatarElement = (
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div>
|
||||
<div>
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
|
@ -1171,10 +1191,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
title={_t('Close')} />;
|
||||
}
|
||||
|
||||
const memberDetails = <PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
/>;
|
||||
const memberDetails = (
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
);
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||
|
@ -1253,18 +1277,28 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
|
||||
const devicesSection = isRoomEncrypted ?
|
||||
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
|
||||
|
||||
const userVerified = cli.checkUserTrust(user.userId).isVerified();
|
||||
let verifyButton;
|
||||
if (!userVerified) {
|
||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton>
|
||||
{verifyButton}
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
|
||||
let e2eIcon;
|
||||
if (isRoomEncrypted && devices) {
|
||||
e2eIcon = <E2EIcon size={18} status={_getE2EStatus(devices)} isUser={true} />;
|
||||
const e2eStatus = _getE2EStatus(cli, user.userId, devices);
|
||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1300,7 +1334,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
devices={devices}
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={user} />
|
||||
member={member} />
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
||||
|
@ -1308,7 +1342,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
UserInfo.propTypes = {
|
||||
user: PropTypes.oneOfType([
|
||||
|
|
|
@ -15,14 +15,68 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
<<<<<<< HEAD
|
||||
import React from "react";
|
||||
=======
|
||||
import EditableItemList from "../elements/EditableItemList";
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
>>>>>>> develop
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
<<<<<<< HEAD
|
||||
import Modal from "../../../Modal";
|
||||
=======
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
const Modal = require("../../../Modal");
|
||||
>>>>>>> develop
|
||||
|
||||
class EditableAliasesList extends EditableItemList {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._aliasField = createRef();
|
||||
}
|
||||
|
||||
_onAliasAdded = async () => {
|
||||
await this._aliasField.current.validate({ allowEmpty: false });
|
||||
|
||||
if (this._aliasField.current.isValid) {
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
return;
|
||||
}
|
||||
|
||||
this._aliasField.current.focus();
|
||||
this._aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onAliasAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<RoomAliasField
|
||||
id={`mx_EditableItemList_new_${this.props.id}`}
|
||||
ref={this._aliasField}
|
||||
onChange={onChange}
|
||||
value={this.props.newItem || ""}
|
||||
domain={this.props.domain} />
|
||||
<AccessibleButton onClick={this._onAliasAdded} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AliasSettings extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,7 +101,6 @@ export default class AliasSettings extends React.Component {
|
|||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
updatingCanonicalAlias: false,
|
||||
newItem: "",
|
||||
};
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
@ -73,11 +126,6 @@ export default class AliasSettings extends React.Component {
|
|||
return dict;
|
||||
}
|
||||
|
||||
isAliasValid(alias) {
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return (alias.match(/^#([^/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||
}
|
||||
|
||||
changeCanonicalAlias(alias) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
|
@ -113,38 +161,31 @@ export default class AliasSettings extends React.Component {
|
|||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
});
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
if (!this.state.canonicalAlias) {
|
||||
this.changeCanonicalAlias(alias);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
|
||||
title: _t("Error creating alias"),
|
||||
description: _t(
|
||||
"There was an error creating that alias. It may not be allowed by the server " +
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||
title: _t('Invalid alias format'),
|
||||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||
|
||||
if (!this.state.canonicalAlias) {
|
||||
this.changeCanonicalAlias(alias);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
|
||||
title: _t("Error creating alias"),
|
||||
description: _t(
|
||||
"There was an error creating that alias. It may not be allowed by the server " +
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onLocalAliasDeleted = (index) => {
|
||||
|
@ -181,7 +222,6 @@ export default class AliasSettings extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
let found = false;
|
||||
|
@ -233,7 +273,7 @@ export default class AliasSettings extends React.Component {
|
|||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
{canonicalAliasSection}
|
||||
<EditableItemList
|
||||
<EditableAliasesList
|
||||
id="roomAliases"
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
|
@ -248,6 +288,7 @@ export default class AliasSettings extends React.Component {
|
|||
placeholder={_t(
|
||||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
domain={localDomain}
|
||||
/>
|
||||
{remoteAliasesSection}
|
||||
</div>
|
||||
|
|
|
@ -16,11 +16,17 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
<<<<<<< HEAD
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import * as sdk from '../../../index';
|
||||
=======
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
>>>>>>> develop
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+:\S+/;
|
||||
|
||||
|
@ -31,9 +37,7 @@ export default class RelatedGroupSettings extends React.Component {
|
|||
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
static defaultProps = {
|
||||
canSetRelatedGroups: false,
|
||||
|
@ -49,7 +53,7 @@ export default class RelatedGroupSettings extends React.Component {
|
|||
}
|
||||
|
||||
updateGroups(newGroupsList) {
|
||||
this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
|
||||
this.context.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
|
||||
groups: newGroupsList,
|
||||
}, '').catch((err) => {
|
||||
console.error(err);
|
||||
|
@ -99,7 +103,7 @@ export default class RelatedGroupSettings extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const localDomain = this.context.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return <div>
|
||||
<EditableItemList
|
||||
|
|
|
@ -19,8 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
|
||||
// TODO: Merge with ProfileSettings?
|
||||
export default class RoomProfileSettings extends React.Component {
|
||||
|
@ -62,13 +61,20 @@ export default class RoomProfileSettings extends React.Component {
|
|||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
_uploadAvatar = () => {
|
||||
this._avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_removeAvatar = () => {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -92,6 +98,8 @@ export default class RoomProfileSettings extends React.Component {
|
|||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, '');
|
||||
}
|
||||
|
||||
if (this.state.originalTopic !== this.state.topic) {
|
||||
|
@ -139,45 +147,8 @@ export default class RoomProfileSettings extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
|
||||
|
||||
let showOverlayAnyways = true;
|
||||
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
|
||||
if (this.state.avatarUrl) {
|
||||
showOverlayAnyways = false;
|
||||
avatarElement = <img src={this.state.avatarUrl}
|
||||
alt={_t("Room avatar")} />;
|
||||
}
|
||||
|
||||
const avatarOverlayClasses = classNames({
|
||||
"mx_ProfileSettings_avatarOverlay": true,
|
||||
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
|
||||
});
|
||||
let avatarHoverElement = (
|
||||
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
|
||||
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload room avatar")}</span>
|
||||
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
|
||||
<div className="mx_ProfileSettings_avatarOverlayImg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (!this.state.canSetAvatar) {
|
||||
if (!showOverlayAnyways) {
|
||||
avatarHoverElement = null;
|
||||
} else {
|
||||
const disabledOverlayClasses = classNames({
|
||||
"mx_ProfileSettings_avatarOverlay": true,
|
||||
"mx_ProfileSettings_avatarOverlay_show": true,
|
||||
"mx_ProfileSettings_avatarOverlay_disabled": true,
|
||||
});
|
||||
avatarHoverElement = (
|
||||
<div className={disabledOverlayClasses}>
|
||||
<span className="mx_ProfileSettings_noAvatarText">{_t("No room avatar")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
|
@ -191,10 +162,12 @@ export default class RoomProfileSettings extends React.Component {
|
|||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_avatar">
|
||||
{avatarElement}
|
||||
{avatarHoverElement}
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.props.roomId}
|
||||
avatarAltText={_t("Room avatar")}
|
||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
|
|
|
@ -34,11 +34,11 @@ import {parsePlainTextMessage} from '../../../editor/deserialize';
|
|||
import {renderModel} from '../../../editor/render';
|
||||
import {Room} from 'matrix-js-sdk';
|
||||
import TypingStore from "../../../stores/TypingStore";
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import * as sdk from '../../../index';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {EMOTICON_TO_EMOJI} from "../../../emoji";
|
||||
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
|
@ -80,8 +80,8 @@ export default class BasicMessageEditor extends React.Component {
|
|||
initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autoComplete: null,
|
||||
};
|
||||
|
@ -108,7 +108,8 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
||||
if (emoticonMatch) {
|
||||
const query = emoticonMatch[1].toLowerCase().replace("-", "");
|
||||
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false);
|
||||
const data = EMOTICON_TO_EMOJI.get(query);
|
||||
|
||||
if (data) {
|
||||
const {partCreator} = model;
|
||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
||||
|
|
|
@ -17,24 +17,62 @@ limitations under the License.
|
|||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export default function(props) {
|
||||
const { isUser } = props;
|
||||
const isNormal = props.status === "normal";
|
||||
const isWarning = props.status === "warning";
|
||||
const isVerified = props.status === "verified";
|
||||
const e2eIconClasses = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_warning: isWarning,
|
||||
mx_E2EIcon_normal: isNormal,
|
||||
mx_E2EIcon_verified: isVerified,
|
||||
}, props.className);
|
||||
let e2eTitle;
|
||||
if (isWarning) {
|
||||
e2eTitle = props.isUser ?
|
||||
_t("Some devices for this user are not trusted") :
|
||||
_t("Some devices in this encrypted room are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = props.isUser ?
|
||||
_t("All devices for this user are trusted") :
|
||||
_t("All devices in this encrypted room are trusted");
|
||||
|
||||
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
if (crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t(
|
||||
"This user has not verified all of their devices.",
|
||||
);
|
||||
} else if (isNormal) {
|
||||
e2eTitle = _t(
|
||||
"You have not verified this user. " +
|
||||
"This user has verified all of their devices.",
|
||||
);
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t(
|
||||
"You have verified this user. " +
|
||||
"This user has verified all of their devices.",
|
||||
);
|
||||
}
|
||||
} else if (crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t(
|
||||
"Some users in this encrypted room are not verified by you or " +
|
||||
"they have not verified their own devices.",
|
||||
);
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t(
|
||||
"All users in this encrypted room are verified by you and " +
|
||||
"they have verified their own devices.",
|
||||
);
|
||||
}
|
||||
} else if (!crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices for this user are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices for this user are trusted");
|
||||
}
|
||||
} else if (!crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices in this encrypted room are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
}
|
||||
|
||||
let style = null;
|
||||
|
|
|
@ -26,11 +26,11 @@ import {findEditableEvent} from '../../../utils/EventUtils';
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -105,12 +105,10 @@ export default class EditMessageComposer extends React.Component {
|
|||
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.model = null;
|
||||
this._editorRef = null;
|
||||
|
||||
|
@ -124,7 +122,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
};
|
||||
|
||||
_getRoom() {
|
||||
return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
|
@ -190,7 +188,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.context.matrixClient.sendMessage(roomId, editContent);
|
||||
this.context.sendMessage(roomId, editContent);
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
@ -205,7 +203,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.matrixClient.cancelPendingEvent(previousEdit);
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +230,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(room, this.context.matrixClient);
|
||||
const partCreator = new PartCreator(room, this.context);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -23,16 +23,29 @@ import PropTypes from 'prop-types';
|
|||
import createReactClass from 'create-react-class';
|
||||
import classNames from "classnames";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
<<<<<<< HEAD
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import Modal from "../../../Modal";
|
||||
import * as sdk from "../../../index";
|
||||
=======
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
|
||||
>>>>>>> develop
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus, MatrixClient} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
<<<<<<< HEAD
|
||||
import * as ObjectUtils from "../../../ObjectUtils";
|
||||
=======
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
>>>>>>> develop
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -218,8 +231,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -233,7 +246,7 @@ export default createReactClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
|
@ -258,7 +271,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
|
@ -287,7 +300,7 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const verified = await this.context.matrixClient.isEventSenderVerified(mxEvent);
|
||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||
this.setState({
|
||||
verified: verified,
|
||||
}, () => {
|
||||
|
@ -345,11 +358,11 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
shouldHighlight: function() {
|
||||
const actions = this.context.matrixClient.getPushActionsForEvent(this.props.mxEvent);
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
// don't show self-highlights from another of our clients
|
||||
if (this.props.mxEvent.getSender() === this.context.matrixClient.credentials.userId) {
|
||||
if (this.props.mxEvent.getSender() === this.context.credentials.userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -438,15 +451,6 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
onCryptoClick: function(e) {
|
||||
const event = this.props.mxEvent;
|
||||
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
},
|
||||
|
||||
onRequestKeysClick: function() {
|
||||
this.setState({
|
||||
// Indicate in the UI that the keys have been requested (this is expected to
|
||||
|
@ -457,7 +461,7 @@ export default createReactClass({
|
|||
// Cancel any outgoing key request for this event and resend it. If a response
|
||||
// is received for the request with the required keys, the event could be
|
||||
// decrypted successfully.
|
||||
this.context.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
},
|
||||
|
||||
onPermalinkClicked: function(e) {
|
||||
|
@ -474,11 +478,10 @@ export default createReactClass({
|
|||
|
||||
_renderE2EPadlock: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClick};
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable {...props} />;
|
||||
return <E2ePadlockUndecryptable />;
|
||||
}
|
||||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
|
@ -486,11 +489,11 @@ export default createReactClass({
|
|||
if (this.state.verified) {
|
||||
return; // no icon for verified
|
||||
} else {
|
||||
return (<E2ePadlockUnverified {...props} />);
|
||||
return (<E2ePadlockUnverified />);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.context.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
if (this.context.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
|
@ -503,7 +506,7 @@ export default createReactClass({
|
|||
return; // we expect this to be unencrypted
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
return <E2ePadlockUnencrypted />;
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -737,7 +740,7 @@ export default createReactClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
|
@ -915,7 +918,6 @@ class E2ePadlock extends React.Component {
|
|||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -926,10 +928,6 @@ class E2ePadlock extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
if (this.props.onClick) this.props.onClick(e);
|
||||
};
|
||||
|
||||
onHoverStart = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import {Key} from '../../../Keyboard';
|
||||
|
||||
|
||||
export default createReactClass({
|
||||
|
@ -52,8 +52,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
switch (ev.key) {
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,17 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import {linkifyElement} from '../../../HtmlUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
<<<<<<< HEAD
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
=======
|
||||
import { _t } from "../../../languageHandler";
|
||||
>>>>>>> develop
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'LinkPreviewWidget',
|
||||
|
@ -125,6 +130,11 @@ export default createReactClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
||||
// 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 }
|
||||
|
@ -132,12 +142,13 @@ export default createReactClass({
|
|||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="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}>
|
||||
{ p["og:description"] }
|
||||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
<img className="mx_LinkPreviewWidget_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
onClick={this.props.onCancelClick} />
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -23,6 +23,8 @@ import classNames from 'classnames';
|
|||
export default class MemberDeviceInfo extends React.Component {
|
||||
render() {
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
// XXX: These checks are not cross-signing aware but this component is only used
|
||||
// from the old, pre-cross-signing memberinfopanel
|
||||
const iconClasses = classNames({
|
||||
mx_MemberDeviceInfo_icon: true,
|
||||
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
|
||||
|
|
|
@ -31,7 +31,6 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -47,8 +46,13 @@ import MultiInviter from "../../../utils/MultiInviter";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
<<<<<<< HEAD
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {EventTimeline} from "matrix-js-sdk";
|
||||
=======
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
>>>>>>> develop
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -76,13 +80,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
const cli = this.context.matrixClient;
|
||||
const cli = this.context;
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
this._enableDevices = cli.isCryptoEnabled();
|
||||
|
@ -112,7 +116,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.removeListener("Room", this.onRoom);
|
||||
|
@ -131,7 +135,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_checkIgnoreState: function() {
|
||||
const isIgnoring = this.context.matrixClient.isUserIgnored(this.props.member.userId);
|
||||
const isIgnoring = this.context.isUserIgnored(this.props.member.userId);
|
||||
this.setState({isIgnoring: isIgnoring});
|
||||
},
|
||||
|
||||
|
@ -163,7 +167,7 @@ export default createReactClass({
|
|||
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(this.context.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
Promise.resolve(this.context.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
this.setState({
|
||||
devices: devices,
|
||||
e2eStatus: this._getE2EStatus(devices),
|
||||
|
@ -197,7 +201,7 @@ export default createReactClass({
|
|||
onRoomReceipt: function(receiptEvent, room) {
|
||||
// because if we read a notification, it will affect notification count
|
||||
// only bother updating if there's a receipt from us
|
||||
if (findReadReceiptFromUserId(receiptEvent, this.context.matrixClient.credentials.userId)) {
|
||||
if (findReadReceiptFromUserId(receiptEvent, this.context.credentials.userId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
@ -242,7 +246,7 @@ export default createReactClass({
|
|||
let cancelled = false;
|
||||
this._cancelDeviceList = function() { cancelled = true; };
|
||||
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
const self = this;
|
||||
client.downloadKeys([member.userId], true).then(() => {
|
||||
return client.getStoredDevicesForUser(member.userId);
|
||||
|
@ -267,7 +271,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onIgnoreToggle: function() {
|
||||
const ignoredUsers = this.context.matrixClient.getIgnoredUsers();
|
||||
const ignoredUsers = this.context.getIgnoredUsers();
|
||||
if (this.state.isIgnoring) {
|
||||
const index = ignoredUsers.indexOf(this.props.member.userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
|
@ -275,7 +279,7 @@ export default createReactClass({
|
|||
ignoredUsers.push(this.props.member.userId);
|
||||
}
|
||||
|
||||
this.context.matrixClient.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
this.context.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
return this.setState({isIgnoring: !this.state.isIgnoring});
|
||||
});
|
||||
},
|
||||
|
@ -293,7 +297,7 @@ export default createReactClass({
|
|||
if (!proceed) return;
|
||||
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.kick(
|
||||
this.context.kick(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
reason || undefined,
|
||||
).then(function() {
|
||||
|
@ -329,11 +333,11 @@ export default createReactClass({
|
|||
this.setState({ updating: this.state.updating + 1 });
|
||||
let promise;
|
||||
if (this.props.member.membership === 'ban') {
|
||||
promise = this.context.matrixClient.unban(
|
||||
promise = this.context.unban(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
);
|
||||
} else {
|
||||
promise = this.context.matrixClient.ban(
|
||||
promise = this.context.ban(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
reason || undefined,
|
||||
);
|
||||
|
@ -360,7 +364,7 @@ export default createReactClass({
|
|||
|
||||
onRedactAllMessages: async function() {
|
||||
const {roomId, userId} = this.props.member;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
@ -414,7 +418,7 @@ export default createReactClass({
|
|||
console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`);
|
||||
await Promise.all(eventsToRedact.map(async event => {
|
||||
try {
|
||||
await this.context.matrixClient.redactEvent(roomId, event.getId());
|
||||
await this.context.redactEvent(roomId, event.getId());
|
||||
} catch (err) {
|
||||
// log and swallow errors
|
||||
console.error("Could not redact", event.getId());
|
||||
|
@ -446,11 +450,11 @@ export default createReactClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.context.matrixClient.getUserId()) {
|
||||
if (target === this.context.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
|
@ -478,7 +482,7 @@ export default createReactClass({
|
|||
|
||||
if (!isNaN(level)) {
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, level, powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -500,13 +504,13 @@ export default createReactClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
const me = room.getMember(this.context.matrixClient.credentials.userId);
|
||||
const me = room.getMember(this.context.credentials.userId);
|
||||
if (!me) return;
|
||||
|
||||
const defaultLevel = powerLevelEvent.getContent().users_default;
|
||||
|
@ -515,7 +519,7 @@ export default createReactClass({
|
|||
// toggle the level
|
||||
const newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -550,7 +554,7 @@ export default createReactClass({
|
|||
danger: true,
|
||||
onFinished: (accepted) => {
|
||||
if (!accepted) return;
|
||||
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => {
|
||||
this.context.deactivateSynapseUser(this.props.member.userId).catch(e => {
|
||||
console.error("Failed to deactivate user");
|
||||
console.error(e);
|
||||
|
||||
|
@ -566,7 +570,7 @@ export default createReactClass({
|
|||
|
||||
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -587,7 +591,7 @@ export default createReactClass({
|
|||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
@ -598,7 +602,7 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const myUserId = this.context.matrixClient.getUserId();
|
||||
const myUserId = this.context.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
|
@ -650,9 +654,9 @@ export default createReactClass({
|
|||
|
||||
_calculateOpsPermissions: async function(member) {
|
||||
let canDeactivate = false;
|
||||
if (this.context.matrixClient) {
|
||||
if (this.context) {
|
||||
try {
|
||||
canDeactivate = await this.context.matrixClient.isSynapseAdministrator();
|
||||
canDeactivate = await this.context.isSynapseAdministrator();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -665,13 +669,13 @@ export default createReactClass({
|
|||
},
|
||||
muted: false,
|
||||
};
|
||||
const room = this.context.matrixClient.getRoom(member.roomId);
|
||||
const room = this.context.getRoom(member.roomId);
|
||||
if (!room) return defaultPerms;
|
||||
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevels) return defaultPerms;
|
||||
|
||||
const me = room.getMember(this.context.matrixClient.credentials.userId);
|
||||
const me = room.getMember(this.context.credentials.userId);
|
||||
if (!me) return defaultPerms;
|
||||
|
||||
const them = member;
|
||||
|
@ -738,7 +742,7 @@ export default createReactClass({
|
|||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
|
@ -797,7 +801,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_renderUserOptions: function() {
|
||||
const cli = this.context.matrixClient;
|
||||
const cli = this.context;
|
||||
const member = this.props.member;
|
||||
|
||||
let ignoreButton = null;
|
||||
|
@ -905,9 +909,9 @@ export default createReactClass({
|
|||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
if (this.props.member.userId !== this.context.matrixClient.credentials.userId) {
|
||||
if (this.props.member.userId !== this.context.credentials.userId) {
|
||||
// TODO: Immutable DMs replaces a lot of this
|
||||
const dmRoomMap = new DMRoomMap(this.context.matrixClient);
|
||||
const dmRoomMap = new DMRoomMap(this.context);
|
||||
// dmRooms will not include dmRooms that we have been invited into but did not join.
|
||||
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
|
||||
// XXX: we potentially want DMs we have been invited to, to also show up here :L
|
||||
|
@ -918,7 +922,7 @@ export default createReactClass({
|
|||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (room) {
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
|
@ -1064,12 +1068,12 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
|
||||
const room = this.context.matrixClient.getRoom(this.props.member.roomId);
|
||||
const room = this.context.getRoom(this.props.member.roomId);
|
||||
const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = this.context.matrixClient.baseUrl;
|
||||
const hsUrl = this.context.baseUrl;
|
||||
let showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
|
@ -1108,7 +1112,7 @@ export default createReactClass({
|
|||
</div>
|
||||
</div>;
|
||||
|
||||
const isEncrypted = this.context.matrixClient.isRoomEncrypted(this.props.member.roomId);
|
||||
const isEncrypted = this.context.isRoomEncrypted(this.props.member.roomId);
|
||||
if (this.state.e2eStatus && isEncrypted) {
|
||||
e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />);
|
||||
}
|
||||
|
@ -1117,7 +1121,7 @@ export default createReactClass({
|
|||
const avatarUrl = this.props.member.getMxcAvatarUrl();
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>;
|
||||
|
|
|
@ -32,7 +32,15 @@ const INITIAL_LOAD_NUM_MEMBERS = 30;
|
|||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
const SHOW_MORE_INCREMENT = 100;
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default createReactClass({
|
||||
=======
|
||||
// Regex applied to filter our punctuation in member names before applying sort, to fuzzy it a little
|
||||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||
|
||||
module.exports = createReactClass({
|
||||
>>>>>>> develop
|
||||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -336,10 +344,13 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
||||
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
|
||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
||||
return nameA.localeCompare(nameB);
|
||||
return nameA.localeCompare(nameB, {
|
||||
ignorePunctuation: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(searchQuery) {
|
||||
|
|
|
@ -107,8 +107,8 @@ class UploadButton extends React.Component {
|
|||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
|
||||
|
@ -165,8 +165,8 @@ class UploadButton extends React.Component {
|
|||
}
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
|
|
|
@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import('../../../VelocityBounce');
|
||||
|
@ -88,6 +87,10 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._avatar = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// before we remove the rr, store its location in the map, so that if
|
||||
// it reappears, it can be animated from the right place.
|
||||
|
@ -103,7 +106,7 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const avatarNode = ReactDOM.findDOMNode(this);
|
||||
const avatarNode = this._avatar.current;
|
||||
rrInfo.top = avatarNode.offsetTop;
|
||||
rrInfo.left = avatarNode.offsetLeft;
|
||||
rrInfo.parent = avatarNode.offsetParent;
|
||||
|
@ -123,7 +126,7 @@ export default createReactClass({
|
|||
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
const newElement = ReactDOM.findDOMNode(this);
|
||||
const newElement = this._avatar.current;
|
||||
let startTopOffset;
|
||||
if (!newElement.offsetParent) {
|
||||
// this seems to happen sometimes for reasons I don't understand
|
||||
|
@ -173,7 +176,7 @@ export default createReactClass({
|
|||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
if (this.state.suppressDisplay) {
|
||||
return <div />;
|
||||
return <div ref={this._avatar} />;
|
||||
}
|
||||
|
||||
const style = {
|
||||
|
@ -213,6 +216,7 @@ export default createReactClass({
|
|||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
inputRef={this._avatar}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
|
|
|
@ -35,8 +35,8 @@ export default class ReplyPreview extends React.Component {
|
|||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.unmounted = false;
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -21,6 +21,7 @@ import React from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import utils from "matrix-js-sdk/lib/utils";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
|
@ -588,10 +589,17 @@ export default createReactClass({
|
|||
_applySearchFilter: function(list, filter) {
|
||||
if (filter === "") return list;
|
||||
const lcFilter = filter.toLowerCase();
|
||||
// apply toLowerCase before and after removeHiddenChars because different rules get applied
|
||||
// e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H
|
||||
const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase();
|
||||
// case insensitive if room name includes filter,
|
||||
// or if starts with `#` and one of room's aliases starts with filter
|
||||
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
return list.filter((room) => {
|
||||
if (filter[0] === "#" && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) {
|
||||
return true;
|
||||
}
|
||||
return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter);
|
||||
});
|
||||
},
|
||||
|
||||
_handleCollapsedState: function(key, collapsed) {
|
||||
|
@ -627,7 +635,6 @@ export default createReactClass({
|
|||
const defaultProps = {
|
||||
collapsed: this.props.collapsed,
|
||||
isFiltered: !!this.props.searchFilter,
|
||||
incomingCall: this.state.incomingCall,
|
||||
};
|
||||
|
||||
subListsProps.forEach((p) => {
|
||||
|
@ -640,7 +647,7 @@ export default createReactClass({
|
|||
}));
|
||||
|
||||
return subListsProps.reduce((components, props, i) => {
|
||||
props = Object.assign({}, defaultProps, props);
|
||||
props = {...defaultProps, ...props};
|
||||
const isLast = i === subListsProps.length - 1;
|
||||
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
|
||||
const {key, label, onHeaderClick, ...otherProps} = props;
|
||||
|
@ -651,12 +658,12 @@ export default createReactClass({
|
|||
onHeaderClick(collapsed);
|
||||
}
|
||||
};
|
||||
let startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
this._layoutSections.push({
|
||||
id: chosenKey,
|
||||
count: len,
|
||||
});
|
||||
let subList = (<RoomSubList
|
||||
const subList = (<RoomSubList
|
||||
ref={this._subListRef.bind(this, chosenKey)}
|
||||
startAsHidden={startAsHidden}
|
||||
forceExpand={!!this.props.searchFilter}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -70,10 +71,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -150,14 +155,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
onClick={this.onSetupClick}>
|
||||
{setupCaption}
|
||||
</AccessibleButton>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onOnNotNowClick}>
|
||||
{ _t("Not now") }
|
||||
</AccessibleButton></p>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("Don't ask me again") }
|
||||
</AccessibleButton></p>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -68,11 +68,6 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
return Boolean(dmRooms);
|
||||
},
|
||||
|
||||
_shouldShowStatusMessage() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
return false;
|
||||
|
@ -371,8 +366,11 @@ export default createReactClass({
|
|||
|
||||
let ariaLabel = name;
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
|
||||
let dmIndicator;
|
||||
if (this._isDirectMessageRoom(this.props.room.roomId)) {
|
||||
let dmOnline;
|
||||
if (dmUserId) {
|
||||
dmIndicator = <img
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomTile_dm"
|
||||
|
@ -380,6 +378,13 @@ export default createReactClass({
|
|||
height="13"
|
||||
alt="dm"
|
||||
/>;
|
||||
|
||||
const { room } = this.props;
|
||||
const member = room.getMember(dmUserId);
|
||||
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
|
||||
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
|
||||
dmOnline = <UserOnlineDot userId={dmUserId} />;
|
||||
}
|
||||
}
|
||||
|
||||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||
|
@ -428,6 +433,7 @@ export default createReactClass({
|
|||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ dmOnline }
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ import createReactClass from 'create-react-class';
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'SearchBar',
|
||||
|
@ -42,11 +43,13 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onSearchChange: function(e) {
|
||||
if (e.keyCode === 13) { // on enter...
|
||||
this.onSearch();
|
||||
}
|
||||
if (e.keyCode === 27) { // escape...
|
||||
this.props.onCancelClick();
|
||||
switch (e.key) {
|
||||
case Key.ENTER:
|
||||
this.onSearch();
|
||||
break;
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
unescapeMessage,
|
||||
} from '../../../editor/serialize';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -40,6 +39,7 @@ import Modal from '../../../Modal';
|
|||
import {_t, _td} from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -89,12 +89,10 @@ export default class SendMessageComposer extends React.Component {
|
|||
permalinkCreator: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.model = null;
|
||||
this._editorRef = null;
|
||||
this.currentlyComposedEditorState = null;
|
||||
|
@ -245,7 +243,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||
this.context.matrixClient.sendMessage(roomId, content);
|
||||
this.context.sendMessage(roomId, content);
|
||||
if (isReply) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -273,7 +271,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const parts = this._restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -361,7 +359,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
// from Finder) but more images copied from a different website
|
||||
// / word processor etc.
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context.matrixClient,
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -315,8 +315,8 @@ export default class Stickerpicker extends React.Component {
|
|||
|
||||
// Offset the chevron location, which is relative to the left of the context menu
|
||||
// (10 = offset when context menu would not be displayed off viewport)
|
||||
// (8 = value required in practice (possibly 10 - 2 where the 2 = context menu borders)
|
||||
const stickerPickerChevronOffset = Math.max(10, 8 + window.pageXOffset + buttonRect.left - x);
|
||||
// (2 = context menu borders)
|
||||
const stickerPickerChevronOffset = Math.max(10, 2 + window.pageXOffset + buttonRect.left - x);
|
||||
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
|
|
48
src/components/views/rooms/UserOnlineDot.js
Normal file
48
src/components/views/rooms/UserOnlineDot.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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.
|
||||
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, useMemo, useState, useCallback} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const UserOnlineDot = ({userId}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const user = useMemo(() => cli.getUser(userId), [cli, userId]);
|
||||
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsOnline(user && (user.currentlyActive || user.presence === "online"));
|
||||
}, [cli, user]);
|
||||
// Recheck also if we receive a User.currentlyActive event
|
||||
const currentlyActiveHandler = useCallback((ev) => {
|
||||
const content = ev.getContent();
|
||||
setIsOnline(content.currently_active || content.presence === "online");
|
||||
}, []);
|
||||
useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler);
|
||||
useEventEmitter(user, "User.presence", currentlyActiveHandler);
|
||||
|
||||
return isOnline ? <span className="mx_UserOnlineDot" /> : null;
|
||||
};
|
||||
|
||||
UserOnlineDot.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default UserOnlineDot;
|
78
src/components/views/settings/AvatarSetting.js
Normal file
78
src/components/views/settings/AvatarSetting.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
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.
|
||||
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, {useCallback} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const openImageView = useCallback(() => {
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
Modal.createDialog(ImageView, {
|
||||
src: avatarUrl,
|
||||
name: avatarName,
|
||||
}, "mx_Dialog_lightbox");
|
||||
}, [avatarUrl, avatarName]);
|
||||
|
||||
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
|
||||
if (avatarUrl) {
|
||||
avatarElement = (
|
||||
<AccessibleButton
|
||||
element="img"
|
||||
src={avatarUrl}
|
||||
alt={avatarAltText}
|
||||
aria-label={avatarAltText}
|
||||
onClick={openImageView} />
|
||||
);
|
||||
}
|
||||
|
||||
let uploadAvatarBtn;
|
||||
if (uploadAvatar) {
|
||||
// insert an empty div to be the host for a css mask containing the upload.svg
|
||||
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
|
||||
<div> </div>
|
||||
{_t("Upload")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let removeAvatarBtn;
|
||||
if (avatarUrl && removeAvatar) {
|
||||
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_AvatarSetting_avatar">
|
||||
{ avatarElement }
|
||||
{ uploadAvatarBtn }
|
||||
{ removeAvatarBtn }
|
||||
</div>;
|
||||
};
|
||||
|
||||
AvatarSetting.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
avatarName: PropTypes.string.isRequired, // name of user/room the avatar belongs to
|
||||
uploadAvatar: PropTypes.func,
|
||||
removeAvatar: PropTypes.func,
|
||||
avatarAltText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AvatarSetting;
|
|
@ -25,8 +25,8 @@ import { _t } from '../../../languageHandler';
|
|||
import Modal from '../../../Modal';
|
||||
|
||||
export default class DevicesPanel extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
devices: undefined,
|
||||
|
|
|
@ -23,8 +23,8 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import {formatDate} from '../../../DateUtils';
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.onDeviceToggled = this.onDeviceToggled.bind(this);
|
||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export default class IntegrationManager extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -52,7 +53,7 @@ export default class IntegrationManager extends React.Component {
|
|||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.keyCode === 27) { // escape
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 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,8 +21,12 @@ import * as sdk from '../../../index';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
<<<<<<< HEAD
|
||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
=======
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
>>>>>>> develop
|
||||
|
||||
export default class KeyBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -128,36 +132,24 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
secureSecretStorage: false,
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
_startNewBackupWithSecureSecretStorage = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
let info;
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
info = await cli.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
info = await cli.createKeyBackupVersion(info);
|
||||
});
|
||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||
this._loadBackupStatus();
|
||||
} catch (e) {
|
||||
console.error("Error creating key backup", e);
|
||||
// TODO: If creating a version succeeds, but backup fails, should we
|
||||
// delete the version, disable backup, or do nothing? If we just
|
||||
// disable without deleting, we'll enable on next app reload since
|
||||
// it is trusted.
|
||||
if (info && info.version) {
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
|
||||
}
|
||||
}
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
secureSecretStorage: true,
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
_deleteBackup = () => {
|
||||
|
@ -181,22 +173,11 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
_restoreBackup = async () => {
|
||||
// Use legacy path if backup key not stored in secret storage
|
||||
if (!this.state.backupKeyStored) {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
}
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -270,7 +251,7 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
{sub}
|
||||
</span>;
|
||||
const verify = sub =>
|
||||
<span className={sig.device && sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
</span>;
|
||||
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
||||
|
|
|
@ -18,10 +18,9 @@ import React, {createRef} from 'react';
|
|||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from 'classnames';
|
||||
import {User} from "matrix-js-sdk";
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class ProfileSettings extends React.Component {
|
||||
constructor() {
|
||||
|
@ -52,13 +51,20 @@ export default class ProfileSettings extends React.Component {
|
|||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
_uploadAvatar = () => {
|
||||
this._avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_removeAvatar = () => {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -82,6 +88,8 @@ export default class ProfileSettings extends React.Component {
|
|||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
@ -117,29 +125,6 @@ export default class ProfileSettings extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
|
||||
|
||||
let showOverlayAnyways = true;
|
||||
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
|
||||
if (this.state.avatarUrl) {
|
||||
showOverlayAnyways = false;
|
||||
avatarElement = <img src={this.state.avatarUrl}
|
||||
alt={_t("Profile picture")} />;
|
||||
}
|
||||
|
||||
const avatarOverlayClasses = classNames({
|
||||
"mx_ProfileSettings_avatarOverlay": true,
|
||||
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
|
||||
});
|
||||
const avatarHoverElement = (
|
||||
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
|
||||
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload profile picture")}</span>
|
||||
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
|
||||
<div className="mx_ProfileSettings_avatarOverlayImg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const hostingSignupLink = getHostingLink('user-settings');
|
||||
let hostingSignup = null;
|
||||
if (hostingSignupLink) {
|
||||
|
@ -156,6 +141,8 @@ export default class ProfileSettings extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
|
@ -170,10 +157,12 @@ export default class ProfileSettings extends React.Component {
|
|||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_avatar">
|
||||
{avatarElement}
|
||||
{avatarHoverElement}
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.state.userId}
|
||||
avatarAltText={_t("Profile picture")}
|
||||
uploadAvatar={this._uploadAvatar}
|
||||
removeAvatar={this._removeAvatar} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
|
|
166
src/components/views/settings/tabs/room/BridgeSettingsTab.js
Normal file
166
src/components/views/settings/tabs/room/BridgeSettingsTab.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
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.
|
||||
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 PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import Pill from "../../../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../../../avatars/BaseAvatar";
|
||||
import { ContentRepo } from "matrix-js-sdk";
|
||||
|
||||
const BRIDGE_EVENT_TYPES = [
|
||||
"uk.half-shot.bridge",
|
||||
// m.bridge
|
||||
];
|
||||
|
||||
export default class BridgeSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
_renderBridgeCard(event, room) {
|
||||
const content = event.getContent();
|
||||
if (!content || !content.channel || !content.protocol) {
|
||||
return null;
|
||||
}
|
||||
const { channel, network } = content;
|
||||
const protocolName = content.protocol.displayname || content.protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
const networkName = network ? network.displayname || network.id : protocolName;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = <p> { _t("This bridge was provisioned by <user />", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={true}
|
||||
/>,
|
||||
})}</p>;
|
||||
}
|
||||
|
||||
const bot = (<p> {_t("This bridge is managed by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={room}
|
||||
url={makeUserPermalink(event.getSender())}
|
||||
shouldShowPillAvatar={true}
|
||||
/>,
|
||||
})} </p>);
|
||||
let channelLink = channelName;
|
||||
if (channel.external_url) {
|
||||
channelLink = <a target="_blank" href={channel.external_url} rel="noopener">{channelName}</a>;
|
||||
}
|
||||
|
||||
let networkLink = networkName;
|
||||
if (network && network.external_url) {
|
||||
networkLink = <a target="_blank" href={network.external_url} rel="noopener">{networkName}</a>;
|
||||
}
|
||||
|
||||
const chanAndNetworkInfo = (
|
||||
_t("Bridged into <channelLink /> <networkLink />, on <protocolName />", {}, {
|
||||
channelLink,
|
||||
networkLink,
|
||||
protocolName,
|
||||
})
|
||||
);
|
||||
|
||||
let networkIcon = null;
|
||||
if (networkName && network.avatar) {
|
||||
const avatarUrl = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
network.avatar, 32, 32, "crop",
|
||||
);
|
||||
networkIcon = <BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
name={ networkName }
|
||||
idName={ networkName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
}
|
||||
|
||||
let channelIcon = null;
|
||||
if (channel.avatar) {
|
||||
const avatarUrl = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
channel.avatar, 32, 32, "crop",
|
||||
);
|
||||
channelIcon = <BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
name={ networkName }
|
||||
idName={ networkName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
}
|
||||
|
||||
const heading = _t("Connected to <channelIcon /> <channelName /> on <networkIcon /> <networkName />", { }, {
|
||||
channelIcon,
|
||||
channelName,
|
||||
networkName,
|
||||
networkIcon,
|
||||
});
|
||||
|
||||
return (<li key={event.stateKey}>
|
||||
<div>
|
||||
<h3>{heading}</h3>
|
||||
<p>{_t("Connected via %(protocolName)s", { protocolName })}</p>
|
||||
<details>
|
||||
{creator}
|
||||
{bot}
|
||||
<p>{chanAndNetworkInfo}</p>
|
||||
</details>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
|
||||
static getBridgeStateEvents(roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const roomState = (client.getRoom(roomId)).currentState;
|
||||
|
||||
const bridgeEvents = Array.concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
|
||||
Object.values(roomState.events[typeName] || {}),
|
||||
));
|
||||
|
||||
return bridgeEvents;
|
||||
}
|
||||
|
||||
render() {
|
||||
// This settings tab will only be invoked if the following function returns more
|
||||
// than 0 events, so no validation is needed at this stage.
|
||||
const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.props.roomId);
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Bridge Info")}</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<p>{ _t("Below is a list of bridges connected to this room.") }</p>
|
||||
<ul className="mx_RoomSettingsDialog_BridgeList">
|
||||
{ bridgeEvents.map((event) => this._renderBridgeCard(event, room)) }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,22 +18,24 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
<<<<<<< HEAD
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
=======
|
||||
import sdk from "../../../../..";
|
||||
>>>>>>> develop
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from "../../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -42,14 +44,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
MatrixClientPeg.get().getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.context.getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
@ -59,7 +55,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(
|
||||
this.context.setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
|
@ -80,7 +76,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const client = this.context;
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||
|
|
|
@ -17,25 +17,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import GroupUserSettings from "../../../groups/GroupUserSettings";
|
||||
<<<<<<< HEAD
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
=======
|
||||
>>>>>>> develop
|
||||
|
||||
export default class FlairUserSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
|
|
|
@ -26,7 +26,6 @@ import PlatformPeg from "../../../../../PlatformPeg";
|
|||
|
||||
export default class PreferencesUserSettingsTab extends React.Component {
|
||||
static COMPOSER_SETTINGS = [
|
||||
'useCiderComposer',
|
||||
'MessageComposerInput.autoReplaceEmoji',
|
||||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
|
|
|
@ -37,12 +37,13 @@ export class IgnoredUser extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
||||
return (
|
||||
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
|
||||
{_t('Unignore')}
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id}>
|
||||
{ _t('Unignore') }
|
||||
</AccessibleButton>
|
||||
<span>{this.props.userId}</span>
|
||||
<span id={id}>{ this.props.userId }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,9 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
|
||||
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
||||
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {verifier});
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue