Merge branch 'develop' into travis/remove-presence

This commit is contained in:
Travis Ralston 2018-04-11 15:17:28 -06:00
commit fe2cbc584d
246 changed files with 16884 additions and 4954 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,6 +16,8 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
@ -23,16 +26,20 @@ module.exports = React.createClass({
displayName: 'BaseAvatar',
propTypes: {
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
idName: React.PropTypes.string, // ID for generating hash colours
title: React.PropTypes.string, // onHover title text
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number,
height: React.PropTypes.number,
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
@ -48,6 +55,16 @@ module.exports = React.createClass({
return this._getState(this.props);
},
componentWillMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync);
},
componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
@ -66,6 +83,23 @@ module.exports = React.createClass({
}
},
onClientSync: function(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING or PREPARED.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected &&
// Did we fall back?
this.state.urlsIndex > 0
) {
// Start from the highest priority URL again
this.setState({
urlsIndex: 0,
});
}
},
_getState: function(props) {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const Avatar = require('../../../Avatar');
const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
@ -25,15 +26,15 @@ module.exports = React.createClass({
displayName: 'MemberAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
// The onClick to give the avatar
onClick: React.PropTypes.func,
onClick: PropTypes.func,
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
viewUserOnClick: React.PropTypes.bool,
title: React.PropTypes.string,
viewUserOnClick: PropTypes.bool,
title: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from "../../../index";
@ -25,11 +26,11 @@ module.exports = React.createClass({
// oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from)
propTypes: {
room: React.PropTypes.object,
oobData: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
room: PropTypes.object,
oobData: PropTypes.object,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
},
getDefaultProps: function() {
@ -47,12 +48,34 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
}
},
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps),
});
},
onRoomStateEvents: function(ev) {
if (!this.props.room ||
ev.getRoomId() !== this.props.room.roomId ||
ev.getType() !== 'm.room.avatar'
) return;
this.setState({
urls: this.getImageUrls(this.props),
});
},
getImageUrls: function(props) {
return [
ContentRepo.getHttpUriForMxc(
@ -86,10 +109,15 @@ module.exports = React.createClass({
const mlist = props.room.currentState.members;
const userIds = [];
const leftUserIds = [];
// for .. in optimisation to return early if there are >2 keys
for (const uid in mlist) {
if (mlist.hasOwnProperty(uid)) {
userIds.push(uid);
if (["join", "invite"].includes(mlist[uid].membership)) {
userIds.push(uid);
} else {
leftUserIds.push(uid);
}
}
if (userIds.length > 2) {
return null;
@ -111,6 +139,14 @@ module.exports = React.createClass({
false,
);
} else if (userIds.length == 1) {
// The other 1-1 user left, leaving just the current user, so show the left user's avatar
if (leftUserIds.length === 1) {
return mlist[leftUserIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
false,
);
}
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),

View file

@ -17,11 +17,12 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: React.PropTypes.func,
onCreateRoom: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const Presets = {
@ -28,8 +29,8 @@ const Presets = {
module.exports = React.createClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: React.PropTypes.func,
preset: React.PropTypes.string,
onChange: PropTypes.func,
preset: PropTypes.string,
},
Presets: Presets,

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -22,9 +23,9 @@ module.exports = React.createClass({
propTypes: {
// Specifying a homeserver will make magical things happen when you,
// e.g. start typing in the room alias box.
homeserver: React.PropTypes.string,
alias: React.PropTypes.string,
onChange: React.PropTypes.func,
homeserver: PropTypes.string,
alias: PropTypes.string,
onChange: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
@ -507,7 +506,8 @@ module.exports = React.createClass({
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
@ -580,14 +580,8 @@ module.exports = React.createClass({
}
return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{ this.props.title }
</div>
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished} title={this.props.title}>
<div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>
@ -597,12 +591,10 @@ module.exports = React.createClass({
{ addressSelector }
{ this.props.extraNode }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
{ this.props.button }
</button>
</div>
</div>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onButtonClick}
onCancel={this.onCancel} />
</BaseDialog>
);
},
});

View file

@ -15,10 +15,15 @@ limitations under the License.
*/
import React from 'react';
import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* Basic container for modal dialogs.
@ -31,33 +36,48 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: React.PropTypes.func,
// called when a key is pressed
onKeyDown: PropTypes.func,
// CSS class to apply to dialog div
className: React.PropTypes.string,
className: PropTypes.string,
// Title for the dialog.
// (could probably actually be something more complicated than a string if desired)
title: React.PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
// children should be the content of the dialog
children: React.PropTypes.node,
children: PropTypes.node,
// Id of content element
// If provided, this is used to add a aria-describedby attribute
contentId: React.PropTypes.string,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
};
},
componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
_onKeyDown: function(e) {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
} else if (e.keyCode === KeyCode.ENTER) {
if (this.props.onEnterPressed) {
e.stopPropagation();
e.preventDefault();
this.props.onEnterPressed(e);
}
}
},
@ -69,17 +89,28 @@ export default React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<FocusTrap onKeyDown={this._onKeyDown}
className={this.props.className}
role="dialog"
aria-labelledby='mx_BaseDialog_title'
// This should point to a node describing the dialog.
// If we were about to completelly 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}
>
<AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<div className='mx_Dialog_title'>
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title }
</div>
{ this.props.children }
</div>
</FocusTrap>
);
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -58,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
);
tiles.push(
<RoomTile key={room.roomId} room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
@ -127,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
@ -137,6 +139,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
@ -144,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
profile = <div className="error" role="alert">
Unable to load profile information for { this.props.userId }
</div>;
} else {
@ -160,17 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
<DialogButtons primaryButton={_t('Start Chatting')}
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
</div>;
}
@ -179,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={this.props.onFinished.bind(false)}
title={title}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
@ -187,9 +188,9 @@ export default class ChatCreateOrReuseDialog extends React.Component {
}
ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
onNewDMClick: PropTypes.func.isRequired,
onExistingRoomSelected: PropTypes.func.isRequired,
};

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
import { GroupMemberType } from '../../../groups';
/*
@ -33,20 +33,20 @@ export default React.createClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: React.PropTypes.object,
member: PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
// needed if a group member is specified
matrixClient: React.PropTypes.instanceOf(MatrixClient),
action: React.PropTypes.string.isRequired, // eg. 'Ban'
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
matrixClient: PropTypes.instanceOf(MatrixClient),
action: PropTypes.string.isRequired, // eg. 'Ban'
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason: React.PropTypes.bool,
danger: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
defaultProps: {
@ -76,13 +76,11 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': this.props.danger,
});
const confirmButtonClass = this.props.danger ? 'danger' : '';
let reasonBox;
if (this.props.askReason) {
@ -116,10 +114,10 @@ export default React.createClass({
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div id="mx_Dialog_content" className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
{ avatar }
</div>
@ -127,17 +125,11 @@ export default React.createClass({
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
</div>
{ reasonBox }
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass}
onClick={this.onOk} autoFocus={!this.props.askReason}
>
{ this.props.action }
</button>
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
<DialogButtons primaryButton={this.props.action}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={confirmButtonClass}
focus={!this.props.askReason}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -55,11 +55,15 @@ export default React.createClass({
_checkGroupId: function(e) {
let error = null;
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
if (!this.state.groupId) {
error = _t("Community IDs cannot not be empty.");
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({
groupIdError: error,
// Reset createError to get rid of now stale error message
createError: null,
});
return error;
},
@ -108,7 +112,7 @@ export default React.createClass({
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
createErrorNode = <div className="error">
createErrorNode = <div className="error" role="alert">
<div>{ _t('Something went wrong whilst creating your community') }</div>
<div>{ this.state.createError.message }</div>
</div>;
@ -116,7 +120,6 @@ export default React.createClass({
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Community')}
>
<form onSubmit={this._onFormSubmit}>
@ -159,10 +162,10 @@ export default React.createClass({
{ createErrorNode }
</div>
<div className="mx_Dialog_buttons">
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
<button onClick={this._onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
</div>
</form>
</BaseDialog>

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
@ -41,40 +42,37 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={_t('Create Room')}
>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ _t('Create Room') }
</button>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
</details>
</div>
</form>
<DialogButtons primaryButton={_t('Create Room')}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Analytics from '../../../Analytics';
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
}
return (
<div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger">
{ _t("Deactivate Account") }
</div>
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}>
<div className="mx_Dialog_content">
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
{ cancelButton }
</div>
</div>
</BaseDialog>
);
}
}
DeactivateAccountDialog.propTypes = {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
}
DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -26,20 +26,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'ErrorDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -51,22 +52,18 @@ export default React.createClass({
};
},
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content">
title={this.props.title || _t('Error')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description || _t('An error has occurred.') }
</div>
<div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
</div>

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -27,22 +28,22 @@ export default React.createClass({
propTypes: {
// matrix client to use for UI auth requests
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
flows: React.PropTypes.array,
params: React.PropTypes.object,
session: React.PropTypes.string,
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback
makeRequest: React.PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
title: React.PropTypes.string,
title: PropTypes.string,
},
getInitialState: function() {
@ -72,11 +73,12 @@ export default React.createClass({
let content;
if (this.state.authError) {
content = (
<div>
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
<div id='mx_Dialog_content'>
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
<br />
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
autoFocus="true"
>
{ _t("Dismiss") }
</AccessibleButton>
@ -84,7 +86,7 @@ export default React.createClass({
);
} else {
content = (
<div>
<div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
@ -99,6 +101,7 @@ export default React.createClass({
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>

View file

@ -16,6 +16,7 @@ limitations under the License.
import Modal from '../../../Modal';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
*/
export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
deviceId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -125,11 +126,11 @@ export default React.createClass({
text = _t(text, {displayName: displayName});
return (
<div>
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
@ -153,7 +154,7 @@ export default React.createClass({
content = this._renderContent();
} else {
content = (
<div>
<div id='mx_Dialog_content'>
<p>{ _t('Loading device info...') }</p>
<Spinner />
</div>
@ -164,6 +165,7 @@ export default React.createClass({
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>

View file

@ -16,20 +16,20 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({
displayName: 'QuestionDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.node,
extraButtons: React.PropTypes.node,
button: React.PropTypes.string,
danger: React.PropTypes.bool,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
title: PropTypes.string,
description: PropTypes.node,
extraButtons: PropTypes.node,
button: PropTypes.string,
danger: PropTypes.bool,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -53,30 +53,27 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
) : null;
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = "";
if (this.props.danger) {
primaryButtonClass = "danger";
}
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description }
</div>
<div className="mx_Dialog_buttons">
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
<DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={primaryButtonClass}
focus={this.props.focus}
onCancel={this.onCancel}
>
{ this.props.extraButtons }
{ cancelButton }
</div>
</DialogButtons>
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
@ -25,8 +26,14 @@ export default React.createClass({
displayName: 'SessionRestoreErrorDialog',
propTypes: {
error: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
error: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
if (this.refs.bugreportLink) {
this.refs.bugreportLink.focus();
}
},
_sendBugReport: function() {
@ -40,6 +47,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bugreport;
if (SdkConfig.get().bug_report_endpoint_url) {
@ -48,16 +56,20 @@ export default React.createClass({
{ _t(
"Otherwise, <a>click here</a> to send a bug report.",
{},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
key="bugreport" href='#'>{ sub }</a> },
) }
</p>
);
}
const shouldFocusContinueButton =!(bugreport==true);
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}>
<div className="mx_Dialog_content">
title={_t('Unable to restore session')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{ _t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.") }</p>
@ -68,11 +80,9 @@ export default React.createClass({
{ bugreport }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
{ _t("Continue anyway") }
</button>
</div>
<DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
onCancel={this.props.onFinished} />
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
export default React.createClass({
displayName: 'SetEmailDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -40,9 +41,6 @@ export default React.createClass({
};
},
componentDidMount: function() {
},
onEmailAddressChanged: function(value) {
this.setState({
emailAddress: value,
@ -130,6 +128,7 @@ export default React.createClass({
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input"
autoFocus="true"
placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={false}
@ -139,9 +138,10 @@ export default React.createClass({
<BaseDialog className="mx_SetEmailDialog"
onFinished={this.onCancelled}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<p>
<p id='mx_Dialog_content'>
{ _t('This will allow you to reset your password and receive notifications.') }
</p>
{ emailInput }

View file

@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
onDifferentServerClicked: PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
onLoginClick: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -234,14 +235,14 @@ export default React.createClass({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
authErrorIndicator = <div className="error" role="alert">
{ this.state.authError }
</div>;
}
@ -253,8 +254,9 @@ export default React.createClass({
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title={_t('To get started, please pick a username!')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}

View file

@ -15,21 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'TextInputDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
value: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
value: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -58,27 +58,24 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ this.props.button }
</button>
</div>
</form>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function markAllDevicesKnown(devices) {
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
}
import { markAllDevicesKnown } from '../../../cryptodevices';
function DeviceListEntry(props) {
const {userId, device} = props;
@ -141,7 +134,7 @@ export default React.createClass({
},
_onSendAnywayClicked: function() {
markAllDevicesKnown(this.props.devices);
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
this.props.onFinished();
this.props.onSend();
@ -153,6 +146,7 @@ export default React.createClass({
},
render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.props.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
@ -187,24 +181,18 @@ export default React.createClass({
}
});
});
let sendButton;
if (haveUnknownDevices) {
sendButton = <button onClick={this._onSendAnywayClicked}>
{ this.props.sendAnywayLabel }
</button>;
} else {
sendButton = <button onClick={this._onSendClicked}>
{ this.props.sendLabel }
</button>;
}
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title={_t('Room contains unknown devices')}
contentId='mx_Dialog_content'
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
<h4>
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4>
@ -212,15 +200,10 @@ export default React.createClass({
{ _t("Unknown devices") }:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
{sendButton}
<button className="mx_Dialog_primary" autoFocus={true}
onClick={this._onDismissClicked}
>
{_t("Dismiss")}
</button>
</div>
</GeminiScrollbarWrapper>
<DialogButtons primaryButton={sendButtonLabel}
onPrimaryButtonClick={sendButtonOnClick}
onCancel={this._onDismissClicked} />
</BaseDialog>
);
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?

View file

@ -15,6 +15,9 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@ -27,8 +30,34 @@ import React from 'react';
export default function AccessibleButton(props) {
const {element, onClick, children, ...restProps} = props;
restProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
// It's because we are using html buttons at a few places e.g. inside dialogs
// And divs which we report as role button to assistive technologies.
// 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) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
@ -44,9 +73,9 @@ export default function AccessibleButton(props) {
* implemented exactly like a normal onClick handler.
*/
AccessibleButton.propTypes = {
children: React.PropTypes.node,
element: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
children: PropTypes.node,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
AccessibleButton.defaultProps = {

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
@ -26,17 +27,17 @@ export default React.createClass({
displayName: 'AddressSelector',
propTypes: {
onSelected: React.PropTypes.func.isRequired,
onSelected: PropTypes.func.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
// Whether to show the address on the address tiles
showAddress: React.PropTypes.bool,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
showAddress: PropTypes.bool,
truncateAt: PropTypes.number.isRequired,
selected: PropTypes.number,
// Element to put as a header on top of the list
header: React.PropTypes.node,
header: PropTypes.node,
},
getInitialState: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
@ -28,9 +29,9 @@ export default React.createClass({
propTypes: {
address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,
canDismiss: PropTypes.bool,
onDismissed: PropTypes.func,
justified: PropTypes.bool,
},
getDefaultProps: function() {

View file

@ -1,4 +1,4 @@
/*
/**
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
@ -19,6 +19,7 @@ limitations under the License.
import url from 'url';
import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
@ -35,32 +36,25 @@ import WidgetUtils from '../../../WidgetUtils';
import dis from '../../../dispatcher';
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
export default React.createClass({
displayName: 'AppTile',
export default class AppTile extends React.Component {
constructor(props) {
super(props);
this.state = this._getNewState(props);
propTypes: {
id: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
room: React.PropTypes.object.isRequired,
type: React.PropTypes.string.isRequired,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: React.PropTypes.bool,
// UserId of the current user
userId: React.PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget
creatorUserId: React.PropTypes.string,
waitForIframeLoad: React.PropTypes.bool,
},
getDefaultProps() {
return {
url: "",
waitForIframeLoad: true,
};
},
this._onWidgetAction = this._onWidgetAction.bind(this);
this._onMessage = this._onMessage.bind(this);
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
this._onSnapshotClick = this._onSnapshotClick.bind(this);
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
this._onInitialLoad = this._onInitialLoad.bind(this);
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
}
/**
* Set initial component state when the App wUrl (widget URL) is being updated.
@ -72,8 +66,8 @@ export default React.createClass({
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
return {
initialising: true, // True while we are mangling the widget URL
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
initialising: true, // True while we are mangling the widget URL
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId,
// Assume that widget has permission to load if we are the user who
@ -82,8 +76,20 @@ export default React.createClass({
error: null,
deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
this.props.whitelistCapabilities : [],
requestedCapabilities: [],
};
},
}
/**
* Does the widget support a given capability
* @param {[type]} capability Capability to check for
* @return {Boolean} True if capability supported
*/
_hasCapability(capability) {
return this.state.allowedCapabilities.some((c) => {return c === capability;});
}
/**
* Add widget instance specific parameters to pass in wUrl
@ -111,11 +117,7 @@ export default React.createClass({
u.query = params;
return u.format();
},
getInitialState() {
return this._getNewState(this.props);
},
}
/**
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
@ -139,7 +141,7 @@ export default React.createClass({
}
}
return false;
},
}
isMixedContent() {
const parentContentProtocol = window.location.protocol;
@ -151,14 +153,36 @@ export default React.createClass({
return true;
}
return false;
},
}
componentWillMount() {
WidgetMessaging.startListening();
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
window.addEventListener('message', this._onMessage, false);
this.setScalarToken();
},
}
componentDidMount() {
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
// postMessaging API
window.addEventListener('message', this._onMessage, false);
// Widget action listeners
this.dispatcherRef = dis.register(this._onWidgetAction);
}
componentWillUnmount() {
// Widget action listeners
dis.unregister(this.dispatcherRef);
// Widget postMessage listeners
try {
if (this.widgetMessaging) {
this.widgetMessaging.stop();
}
} catch (e) {
console.error('Failed to stop listening for widgetMessaging events', e.message);
}
// Jitsi listener
window.removeEventListener('message', this._onMessage);
}
/**
* Adds a scalar token to the widget URL, if required
@ -210,13 +234,7 @@ export default React.createClass({
initialising: false,
});
});
},
componentWillUnmount() {
WidgetMessaging.stopListening();
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
window.removeEventListener('message', this._onMessage);
},
}
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
@ -231,8 +249,10 @@ export default React.createClass({
widgetPageTitle: nextProps.widgetPageTitle,
});
}
},
}
// Legacy Jitsi widget messaging
// TODO -- This should be replaced with the new widget postMessaging API
_onMessage(event) {
if (this.props.type !== 'jitsi') {
return;
@ -250,63 +270,140 @@ export default React.createClass({
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
PlatformPeg.get().setupScreenSharingForIframe(iframe);
}
},
}
_canUserModify() {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
},
}
_onEditClick(e) {
console.log("Edit widget ID ", this.props.id);
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
},
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
this.props.room, 'type_' + this.props.type, this.props.id);
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
}
}
_onSnapshotClick(e) {
console.warn("Requesting widget snapshot");
this.widgetMessaging.getScreenshot()
.catch((err) => {
console.error("Failed to get screenshot", err);
})
.then((screenshot) => {
dis.dispatch({
action: 'picture_snapshot',
file: screenshot,
}, true);
});
}
/* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser
*/
_onDeleteClick() {
if (this._canUserModify()) {
// Show delete confirmation dialog
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) {
return;
}
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).catch((e) => {
console.error('Failed to delete widget', e);
this.setState({deleting: false});
});
},
});
if (this.props.onDeleteClick) {
this.props.onDeleteClick();
} else {
console.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
if (this._canUserModify()) {
// Show delete confirmation dialog
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) {
return;
}
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).catch((e) => {
console.error('Failed to delete widget', e);
}).finally(() => {
this.setState({deleting: false});
});
},
});
} else {
console.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
}
},
}
/**
* Called when widget iframe has finished loading
*/
_onLoaded() {
if (!this.widgetMessaging) {
this._onInitialLoad();
}
}
/**
* Called on initial load of the widget iframe
*/
_onInitialLoad() {
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
requestedCapabilities = requestedCapabilities || [];
// Allow whitelisted capabilities
let requestedWhitelistCapabilies = [];
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
return this.indexOf(e)>=0;
}, this.props.whitelistCapabilities);
if (requestedWhitelistCapabilies.length > 0 ) {
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
requestedWhitelistCapabilies);
}
}
// TODO -- Add UI to warn about and optionally allow requested capabilities
this.setState({
requestedCapabilities,
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
});
if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities);
}
}).catch((err) => {
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
});
this.setState({loading: false});
},
}
_onWidgetAction(payload) {
if (payload.widgetId === this.props.id) {
switch (payload.action) {
case 'm.sticker':
if (this._hasCapability('m.sticker')) {
dis.dispatch({action: 'post_sticker_message', data: payload.data});
} else {
console.warn('Ignoring sticker message. Invalid capability');
}
break;
}
}
}
/**
* Set remote content title on AppTile
@ -320,7 +417,7 @@ export default React.createClass({
}, (err) =>{
console.error("Failed to get page title", err);
});
},
}
// Widget labels to render, depending upon user permissions
// These strings are translated at the point that they are inserted in to the DOM, in the render method
@ -329,20 +426,20 @@ export default React.createClass({
return _td('Delete widget');
}
return _td('Revoke widget access');
},
}
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
_grantWidgetPermission() {
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
localStorage.setItem(this.state.widgetPermissionId, true);
this.setState({hasPermissionToLoad: true});
},
}
_revokeWidgetPermission() {
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
localStorage.removeItem(this.state.widgetPermissionId);
this.setState({hasPermissionToLoad: false});
},
}
formatAppTileName() {
let appTileName = "No name";
@ -350,7 +447,7 @@ export default React.createClass({
appTileName = this.props.name.trim();
}
return appTileName;
},
}
onClickMenuBar(ev) {
ev.preventDefault();
@ -365,16 +462,42 @@ export default React.createClass({
action: 'appsDrawer',
show: !this.props.show,
});
},
}
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
if (ENABLE_REACT_PERF) {
parsedWidgetUrl.search = null;
parsedWidgetUrl.query.react_perf = true;
}
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
},
}
_getTileTitle() {
const name = this.formatAppTileName();
const titleSpacer = <span>&nbsp;-&nbsp;</span>;
let title = '';
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
title = this.state.widgetPageTitle;
}
return (
<span>
<b>{ name }</b>
<span>{ title ? titleSpacer : '' }{ title }</span>
</span>
);
}
_onMinimiseClick(e) {
if (this.props.onMinimiseClick) {
this.props.onMinimiseClick();
}
}
render() {
let appTileBody;
@ -392,9 +515,13 @@ export default React.createClass({
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation";
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media;";
if (this.props.show) {
const loadingElement = (
<div className='mx_AppTileBody mx_AppLoading'>
<div>
<MessageSpinner msg='Loading...' />
</div>
);
@ -409,9 +536,15 @@ export default React.createClass({
);
} else {
appTileBody = (
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
{ this.state.loading && loadingElement }
{ /*
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
"allow" attribute, which is unknown to react 15.
*/ }
<iframe
is
allow={iframeFeatures}
ref="appFrame"
src={this._getSafeUrl()}
allowFullScreen="true"
@ -445,29 +578,42 @@ export default React.createClass({
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
}
// Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
const showPictureSnapshotIcon = 'img/camera_green.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
{ this.props.showMenubar &&
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle">
<TintableSvgButton
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
{ this.props.showMinimise && <TintableSvgButton
src={windowStateIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Minimize apps')}
width="10"
height="10"
/>
<b>{ this.formatAppTileName() }</b>
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
<span>&nbsp;-&nbsp;{ this.state.widgetPageTitle }</span>
) }
onClick={this._onMinimiseClick}
/> }
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <TintableSvgButton
src={showPictureSnapshotIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Picture')}
onClick={this._onSnapshotClick}
width="10"
height="10"
/> }
{ /* Edit widget */ }
{ showEditButton && <TintableSvgButton
src="img/edit_green.svg"
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
className={"mx_AppTileMenuBarWidget " +
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
title={_t('Edit')}
onClick={this._onEditClick}
width="10"
@ -475,18 +621,71 @@ export default React.createClass({
/> }
{ /* Delete widget */ }
<TintableSvgButton
{ this.props.showDelete && <TintableSvgButton
src={deleteIcon}
className={deleteClasses}
title={_t(deleteWidgetLabel)}
onClick={this._onDeleteClick}
width="10"
height="10"
/>
/> }
</span>
</div>
</div> }
{ appTileBody }
</div>
);
},
});
}
}
AppTile.displayName ='AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: PropTypes.bool,
// UserId of the current user
userId: PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget
creatorUserId: PropTypes.string,
waitForIframeLoad: PropTypes.bool,
showMenubar: PropTypes.bool,
// Should the AppTile render itself
show: PropTypes.bool,
// Optional onEditClickHandler (overrides default behaviour)
onEditClick: PropTypes.func,
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick: PropTypes.func,
// Optional onMinimiseClickHandler
onMinimiseClick: PropTypes.func,
// Optionally hide the tile title
showTitle: PropTypes.bool,
// Optionally hide the tile minimise icon
showMinimise: PropTypes.bool,
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the delete icon
showDelete: PropTypes.bool,
// Widget apabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
whitelistCapabilities: PropTypes.array,
// Optional function to be called on widget capability request
// Called with an array of the requested capabilities
onCapabilityRequest: PropTypes.func,
};
AppTile.defaultProps = {
url: "",
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showMinimise: true,
showDelete: true,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
};

View file

@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DragSource, DropTarget } from 'react-dnd';
import TagTile from './TagTile';
import dis from '../../../dispatcher';
import { findDOMNode } from 'react-dom';
const tagTileSource = {
canDrag: function(props, monitor) {
return true;
},
import { Draggable } from 'react-beautiful-dnd';
beginDrag: function(props) {
// Return the data describing the dragged item
return {
tag: props.groupProfile.groupId,
};
},
endDrag: function(props, monitor, component) {
const dropResult = monitor.getDropResult();
if (!monitor.didDrop() || !dropResult) {
return;
}
props.onEndDrag();
},
};
const tagTileTarget = {
canDrop(props, monitor) {
return true;
},
hover(props, monitor, component) {
if (!monitor.canDrop()) return;
const draggedY = monitor.getClientOffset().y;
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
const targetY = (top + bottom) / 2;
dis.dispatch({
action: 'order_tag',
tag: monitor.getItem().tag,
targetTag: props.groupProfile.groupId,
// Note: we indicate that the tag should be after the target when
// it's being dragged over the top half of the target.
after: draggedY < targetY,
});
},
drop(props) {
// Return the data to be returned by getDropResult
return {
tag: props.groupProfile.groupId,
};
},
};
export default
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
}))((props) => {
const { connectDropTarget, connectDragSource, ...otherProps } = props;
return connectDropTarget(connectDragSource(
<div>
<TagTile {...otherProps} />
</div>,
));
}));
export default function DNDTagTile(props) {
return <div>
<Draggable
key={props.tag}
draggableId={props.tag}
index={props.index}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile {...props} />
</div>
{ provided.placeholder }
</div>
) }
</Draggable>
</div>;
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
@ -24,8 +25,8 @@ export default React.createClass({
displayName: 'DeviceVerifyButtons',
propTypes: {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
},
getInitialState: function() {

View file

@ -0,0 +1,62 @@
/*
Copyright 2017 Aidan Gauland
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.
*/
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
/**
* Basic container for buttons in modal dialogs.
*/
module.exports = React.createClass({
displayName: "DialogButtons",
propTypes: {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
// onClick handler for the cancel button.
onCancel: PropTypes.func.isRequired,
focus: PropTypes.bool,
},
render: function() {
let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
return (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
</div>
);
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class DirectorySearchBox extends React.Component {
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
}
DirectorySearchBox.propTypes = {
className: React.PropTypes.string,
onChange: React.PropTypes.func,
onClear: React.PropTypes.func,
onJoinClick: React.PropTypes.func,
placeholder: React.PropTypes.string,
showJoinButton: React.PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
}
MenuOption.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node,
children: PropTypes.oneOfType([
PropTypes.arrayOf(React.PropTypes.node),
PropTypes.node,
]),
highlighted: React.PropTypes.bool,
dropdownKey: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
onMouseEnter: React.PropTypes.func.isRequired,
highlighted: PropTypes.bool,
dropdownKey: PropTypes.string,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
};
/*
@ -322,20 +323,20 @@ Dropdown.propTypes = {
// The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this
// width.
menuWidth: React.PropTypes.number,
menuWidth: PropTypes.number,
// Called when the selected option changes
onOptionChange: React.PropTypes.func.isRequired,
onOptionChange: PropTypes.func.isRequired,
// Called when the value of the search field changes
onSearchChange: React.PropTypes.func,
searchEnabled: React.PropTypes.bool,
onSearchChange: PropTypes.func,
searchEnabled: PropTypes.bool,
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as
// in the dropped-down menu.
getShortOption: React.PropTypes.func,
value: React.PropTypes.string,
getShortOption: PropTypes.func,
value: PropTypes.string,
// negative for consistency with HTML
disabled: React.PropTypes.bool,
disabled: PropTypes.bool,
};

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const KEY_TAB = 9;
const KEY_SHIFT = 16;
@ -26,18 +27,18 @@ module.exports = React.createClass({
displayName: 'EditableText',
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
label: React.PropTypes.string,
placeholder: React.PropTypes.string,
className: React.PropTypes.string,
labelClassName: React.PropTypes.string,
placeholderClassName: React.PropTypes.string,
onValueChanged: PropTypes.func,
initialValue: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
className: PropTypes.string,
labelClassName: PropTypes.string,
placeholderClassName: PropTypes.string,
// Overrides blurToSubmit if true
blurToCancel: React.PropTypes.bool,
blurToCancel: PropTypes.bool,
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: React.PropTypes.bool,
editable: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
editable: PropTypes.bool,
},
Phases: {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Promise from 'bluebird';
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func,
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: React.PropTypes.string,
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: React.PropTypes.string,
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: React.PropTypes.func,
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
};

View file

@ -16,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
@ -32,8 +33,8 @@ export default function EmojiText(props) {
}
EmojiText.propTypes = {
element: React.PropTypes.string,
children: React.PropTypes.string.isRequired,
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {

View file

@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
};
FlairAvatar.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
export default class Flair extends React.Component {
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
}
const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) {
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
this.setState({
profiles: profiles.filter((profile) => {
return profile ? profile.avatarUrl : false;
}),
});
}
}
@ -134,5 +138,5 @@ Flair.propTypes = {
// this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -0,0 +1,35 @@
/*
Copyright 2018 New Vector Ltd.
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 GeminiScrollbar from 'react-gemini-scrollbar';
function GeminiScrollbarWrapper(props) {
const {wrappedRef, ...wrappedProps} = props;
// Enable forceGemini so that gemini is always enabled. This is
// to avoid future issues where a feature is implemented without
// doing QA on every OS/browser combination.
//
// By default GeminiScrollbar allows native scrollbars to be used
// on macOS. Use forceGemini to enable Gemini's non-native
// scrollbars on all OSs.
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
{ props.children }
</GeminiScrollbar>;
}
export default GeminiScrollbarWrapper;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -64,7 +64,7 @@ export default class ManageIntegsButton extends React.Component {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createDialog(IntegrationsManager, {
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
null,
}, "mx_IntegrationsManager");
}
@ -103,5 +103,5 @@ export default class ManageIntegsButton extends React.Component {
}
ManageIntegsButton.propTypes = {
roomId: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
};

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
@ -23,19 +24,19 @@ module.exports = React.createClass({
propTypes: {
// An array of member events to summarise
events: React.PropTypes.array.isRequired,
events: PropTypes.array.isRequired,
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
children: PropTypes.array.isRequired,
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength: React.PropTypes.number,
summaryLength: PropTypes.number,
// The maximum number of avatars to display in the summary
avatarsMaxLength: React.PropTypes.number,
avatarsMaxLength: PropTypes.number,
// The minimum number of events needed to trigger summarisation
threshold: React.PropTypes.number,
threshold: PropTypes.number,
// Called when the MELS expansion is toggled
onToggle: React.PropTypes.func,
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: React.PropTypes.bool,
startExpanded: PropTypes.bool,
},
getInitialState: function() {

View file

@ -17,7 +17,7 @@ import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
@ -61,6 +61,17 @@ const Pill = React.createClass({
shouldShowPillAvatar: PropTypes.bool,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() {
return {
// ID/alias of the room/user
@ -135,6 +146,7 @@ const Pill = React.createClass({
componentWillMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props);
},
@ -195,6 +207,7 @@ const Pill = React.createClass({
const member = this.state.member;
if (member) {
userId = member.userId;
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} />;

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
@ -24,23 +25,26 @@ module.exports = React.createClass({
displayName: 'PowerSelector',
propTypes: {
value: React.PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
maxValue: PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
usersDefault: PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled.
//
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
controlled: PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
disabled: PropTypes.bool,
onChange: PropTypes.func,
// Optional key to pass as the second argument to `onChange`
powerLevelKey: PropTypes.string,
},
getInitialState: function() {
@ -83,17 +87,17 @@ module.exports = React.createClass({
onSelectChange: function(event) {
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
this.props.onChange(event.target.value);
this.props.onChange(event.target.value, this.props.powerLevelKey);
}
},
onCustomBlur: function(event) {
this.props.onChange(parseInt(this.refs.custom.value));
this.props.onChange(parseInt(this.refs.custom.value), this.props.powerLevelKey);
},
onCustomKeyDown: function(event) {
if (event.key == "Enter") {
this.props.onChange(parseInt(this.refs.custom.value));
this.props.onChange(parseInt(this.refs.custom.value), this.props.powerLevelKey);
}
},

View file

@ -17,12 +17,13 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
module.exports = React.createClass({
displayName: 'ProgressBar',
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number,
value: PropTypes.number,
max: PropTypes.number,
},
render: function() {

View file

@ -0,0 +1,188 @@
/*
Copyright 2017 New Vector Ltd
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 sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink} from "../../../matrix-to";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component {
static isMessageUrl(url) {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
}
static childContextTypes = {
matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
};
static propTypes = {
// The matrix.to url of the event
url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event
parentEv: PropTypes.instanceOf(MatrixEvent),
};
constructor(props, context) {
super(props, context);
this.state = {
// The event related to this quote and their nested rich quotes
events: [],
// Whether the top (oldest) event should be shown or spoilered
show: true,
// Whether an error was encountered fetching nested older event, show node if it does
err: false,
};
this.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
};
}
parseUrl(url) {
if (!url) return;
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
const [, roomIdentifier, eventId] = matrixToMatch;
return {roomIdentifier, eventId};
}
componentWillReceiveProps(nextProps) {
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
if (!roomIdentifier || !eventId) return;
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
this.setState({ events: [] });
if (room) this.getEvent(room, eventId, true);
}
componentWillMount() {
this.componentWillReceiveProps(this.props);
}
getRoom(id) {
const cli = MatrixClientPeg.get();
if (id[0] === '!') return cli.getRoom(id);
return cli.getRooms().find((r) => {
return r.getAliases().includes(id);
});
}
async getEvent(room, eventId, show) {
const event = room.findEventById(eventId);
if (event) {
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
this.addEvent(room.findEventById(eventId), show);
}
addEvent(event, show) {
const events = [event].concat(this.state.events);
this.setState({events, show});
}
// addRichQuote(roomId, eventId) {
addRichQuote(href) {
const {roomIdentifier, eventId} = this.parseUrl(href);
if (!roomIdentifier || !eventId) {
this.setState({ err: true });
return;
}
const room = this.getRoom(roomIdentifier);
if (!room) {
this.setState({ err: true });
return;
}
this.getEvent(room, eventId, false);
}
onQuoteClick() {
this.setState({ show: true });
}
render() {
const events = this.state.events.slice();
if (events.length) {
const evTiles = [];
if (!this.state.show) {
const oldestEv = events.shift();
const Pill = sdk.getComponent('elements.Pill');
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
evTiles.push(<blockquote className="mx_Quote" key="load">
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
})
}
</blockquote>);
}
const EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
}
// Deliberately render nothing if the URL isn't recognised
// in case we get an undefined/falsey node, replace it with null to make React happy
return this.props.node || null;
}
}

View file

@ -15,23 +15,24 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
name: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
roomId: PropTypes.string, // for per-room settings
label: PropTypes.string, // untranslated
onChange: PropTypes.func,
isExplicit: PropTypes.bool,
manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
group: PropTypes.string,
value: PropTypes.any, // the value for the radio button
},
getInitialState: function() {

View file

@ -20,36 +20,106 @@ import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
// 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:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
displayName: 'TagTile',
propTypes: {
groupProfile: PropTypes.object,
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
componentWillMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
this._onFlairStoreUpdated();
}
},
componentWillUnmount() {
this.unmounted = true;
if (this.props.tag[0] === '+') {
FlairStore.removeListener('updateGroupProfile', this._onFlairStoreUpdated);
}
},
_onFlairStoreUpdated() {
if (this.unmounted) return;
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.groupProfile.groupId,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onContextButtonClick: function(e) {
e.preventDefault();
e.stopPropagation();
// Hide the (...) immediately
this.setState({ hover: false });
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
const self = this;
ContextualMenu.createMenu(TagTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
top: y,
tag: this.props.tag,
onFinished: function() {
self.setState({ menuDisplayed: false });
},
});
this.setState({ menuDisplayed: true });
},
onMouseOver: function() {
this.setState({hover: true});
},
@ -62,8 +132,8 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.props.groupProfile || {};
const name = profile.name || profile.groupId;
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
@ -78,10 +148,21 @@ export default React.createClass({
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
{ "\u00B7\u00B7\u00B7" }
</div> : <div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
/>
{ tip }
{ contextButton }
</div>
</AccessibleButton>;
},

View file

@ -18,16 +18,17 @@ limitations under the License.
const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const Tinter = require("../../../Tinter");
var TintableSvg = React.createClass({
displayName: 'TintableSvg',
propTypes: {
src: React.PropTypes.string.isRequired,
width: React.PropTypes.string.isRequired,
height: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
},
statics: {

View file

@ -17,14 +17,15 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'UserSelector',
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
onChange: PropTypes.func,
selected_users: PropTypes.arrayOf(React.PropTypes.string),
},
getDefaultProps: function() {

View file

@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
displayName: 'GroupMemberInfo',
@ -132,7 +131,9 @@ module.exports = React.createClass({
render: function() {
if (this.state.removingUser) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
return <div className="mx_MemberInfo">
<Spinner />
</div>;
}
let adminTools;
@ -178,9 +179,10 @@ module.exports = React.createClass({
);
const EmojiText = sdk.getComponent('elements.EmojiText');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
@ -197,7 +199,7 @@ module.exports = React.createClass({
</div>
{ adminTools }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -18,7 +18,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -134,6 +133,7 @@ export default React.createClass({
},
render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.fetching || this.state.fetchingInvitedMembers) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_MemberList">
@ -162,10 +162,10 @@ export default React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
{ joined }
{ invited }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -22,7 +22,6 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
displayName: 'GroupRoomInfo',
@ -157,6 +156,7 @@ module.exports = React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberInfo">
@ -216,7 +216,7 @@ module.exports = React.createClass({
const avatar = <BaseAvatar name={groupRoomName} width={36} height={36} url={avatarUrl} />;
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
@ -233,7 +233,7 @@ module.exports = React.createClass({
</div>
{ adminTools }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -17,7 +17,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_ROOMS = 30;
@ -120,16 +119,17 @@ export default React.createClass({
</form>
);
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{ this.makeGroupRoomTiles(this.state.searchQuery) }
</TruncatedList>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -66,7 +66,7 @@ const GroupRoomTile = React.createClass({
});
GroupRoomTile.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -17,10 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore';
const GroupTile = React.createClass({
displayName: 'GroupTile',
@ -33,7 +35,7 @@ const GroupTile = React.createClass({
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
@ -57,7 +59,7 @@ const GroupTile = React.createClass({
});
},
onClick: function(e) {
onMouseDown: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
@ -77,10 +79,52 @@ const GroupTile = React.createClass({
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div>
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
{ (droppableProvided, droppableSnapshot) => (
<div ref={droppableProvided.innerRef}>
<Draggable
key={"GroupTile " + this.props.groupId}
draggableId={"GroupTile " + this.props.groupId}
index={this.props.groupId}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className="mx_GroupTile_avatar">
<BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div>
</div>
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
{ provided.placeholder ?
<div className="mx_GroupTile_avatar">
<BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div> :
<div />
}
</div>
) }
</Draggable>
</div>
) }
</Droppable>
<div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div>
{ descElement }

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
@ -55,14 +54,15 @@ export default React.createClass({
text = _t('Loading...');
} else if (groups.length > 0) {
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
const groupPublicityToggles = groups.map((groupId, index) => {
return <GroupPublicityToggle key={index} groupId={groupId} />;
});
text = _t('Display your community flair in rooms configured to show it.');
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
<GeminiScrollbar>
<GeminiScrollbarWrapper>
{ groupPublicityToggles }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>;
} else {
text = _t("You're not currently a member of any communities.");

View file

@ -18,6 +18,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha';
@ -29,10 +30,10 @@ module.exports = React.createClass({
displayName: 'CaptchaForm',
propTypes: {
sitePublicKey: React.PropTypes.string,
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: React.PropTypes.func,
onCaptchaResponse: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -17,13 +17,14 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CasLogin',
propTypes: {
onSubmit: React.PropTypes.func, // fn()
onSubmit: PropTypes.func, // fn()
},
render: function() {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@ -131,11 +132,11 @@ export default class CountryDropdown extends React.Component {
}
CountryDropdown.propTypes = {
className: React.PropTypes.string,
isSmall: React.PropTypes.bool,
className: PropTypes.string,
isSmall: PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
disabled: React.PropTypes.bool,
showPrefix: PropTypes.bool,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
disabled: PropTypes.bool,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
@ -69,12 +70,12 @@ export const PasswordAuthEntry = React.createClass({
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: React.PropTypes.bool,
busy: PropTypes.bool,
},
getInitialState: function() {
@ -127,12 +128,22 @@ export const PasswordAuthEntry = React.createClass({
);
}
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t("Password:") }</p>
<form onSubmit={this._onSubmit}>
<label htmlFor="passwordField">{ _t("Password:") }</label>
<input
name="passwordField"
ref="passwordField"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
@ -142,9 +153,7 @@ export const PasswordAuthEntry = React.createClass({
{ submitButtonOrSpinner }
</div>
</form>
<div className="error">
{ this.props.errorText }
</div>
{ errorSection }
</div>
);
},
@ -158,10 +167,10 @@ export const RecaptchaAuthEntry = React.createClass({
},
propTypes: {
submitAuthDict: React.PropTypes.func.isRequired,
stageParams: React.PropTypes.object.isRequired,
errorText: React.PropTypes.string,
busy: React.PropTypes.bool,
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
},
_onCaptchaResponse: function(response) {
@ -179,14 +188,22 @@ export const RecaptchaAuthEntry = React.createClass({
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
const sitePublicKey = this.props.stageParams.public_key;
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse}
/>
<div className="error">
{ this.props.errorText }
</div>
{ errorSection }
</div>
);
},
@ -200,15 +217,15 @@ export const EmailIdentityAuthEntry = React.createClass({
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
authSessionId: React.PropTypes.string.isRequired,
clientSecret: React.PropTypes.string.isRequired,
inputs: React.PropTypes.object.isRequired,
stageState: React.PropTypes.object.isRequired,
fail: React.PropTypes.func.isRequired,
setEmailSid: React.PropTypes.func.isRequired,
makeRegistrationUrl: React.PropTypes.func.isRequired,
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
inputs: PropTypes.object.isRequired,
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -275,15 +292,15 @@ export const MsisdnAuthEntry = React.createClass({
},
propTypes: {
inputs: React.PropTypes.shape({
phoneCountry: React.PropTypes.string,
phoneNumber: React.PropTypes.string,
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
}),
fail: React.PropTypes.func,
clientSecret: React.PropTypes.func,
submitAuthDict: React.PropTypes.func.isRequired,
matrixClient: React.PropTypes.object,
submitAuthDict: React.PropTypes.func,
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
submitAuthDict: PropTypes.func,
},
getInitialState: function() {
@ -371,6 +388,14 @@ export const MsisdnAuthEntry = React.createClass({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_UserSettings_button: true, // XXX button classes
});
let errorSection;
if (this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.state.errorText }
</div>
);
}
return (
<div>
<p>{ _t("A text message has been sent to %(msisdn)s",
@ -384,6 +409,7 @@ export const MsisdnAuthEntry = React.createClass({
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token}
onChange={this._onTokenChange}
aria-label={ _t("Code")}
/>
<br />
<input type="submit" value={_t("Submit")}
@ -391,9 +417,7 @@ export const MsisdnAuthEntry = React.createClass({
disabled={!enableSubmit}
/>
</form>
<div className="error">
{ this.state.errorText }
</div>
{errorSection}
</div>
</div>
);
@ -405,11 +429,11 @@ export const FallbackAuthEntry = React.createClass({
displayName: 'FallbackAuthEntry',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
authSessionId: React.PropTypes.string.isRequired,
loginType: React.PropTypes.string.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
},
componentWillMount: function() {
@ -426,6 +450,12 @@ export const FallbackAuthEntry = React.createClass({
}
},
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
}
},
_onShowFallbackClick: function() {
const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
@ -444,12 +474,18 @@ export const FallbackAuthEntry = React.createClass({
},
render: function() {
return (
<div>
<a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<div className="error">
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);
},

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -256,17 +257,17 @@ PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: React.PropTypes.func, // fn()
initialUsername: React.PropTypes.string,
initialPhoneCountry: React.PropTypes.string,
initialPhoneNumber: React.PropTypes.string,
initialPassword: React.PropTypes.string,
onUsernameChanged: React.PropTypes.func,
onPhoneCountryChanged: React.PropTypes.func,
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { field_input_incorrect } from '../../../UiEffects';
import sdk from '../../../index';
import Email from '../../../email';
@ -40,25 +41,25 @@ module.exports = React.createClass({
propTypes: {
// Values pre-filled in the input boxes when the component loads
defaultEmail: React.PropTypes.string,
defaultPhoneCountry: React.PropTypes.string,
defaultPhoneNumber: React.PropTypes.string,
defaultUsername: React.PropTypes.string,
defaultPassword: React.PropTypes.string,
teamsConfig: React.PropTypes.shape({
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
teamsConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string,
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
supportEmail: PropTypes.string,
teams: PropTypes.arrayOf(React.PropTypes.shape({
// The displayed name of the team
"name": React.PropTypes.string,
"name": PropTypes.string,
// The domain of team email addresses
"domain": React.PropTypes.string,
"domain": PropTypes.string,
})).required,
}),
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
@ -28,24 +29,24 @@ module.exports = React.createClass({
displayName: 'ServerConfig',
propTypes: {
onServerConfigChange: React.PropTypes.func,
onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: React.PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: React.PropTypes.string, // e.g. https://vector.im
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
withToggleButton: React.PropTypes.bool,
delayTimeMs: React.PropTypes.number, // time to wait before invoking onChanged
withToggleButton: PropTypes.bool,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
@ -81,7 +82,7 @@ Tinter.registerTintable(updateTintedDownloadImage);
// downloaded. This limit does not seem to apply when the url is used as
// the source attribute of an image tag.
//
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
// for our purposes they inherit the origin of the page that created them.
// This means that any scripts that run when the URL is viewed will be able
// to access local storage.
@ -191,7 +192,7 @@ module.exports = React.createClass({
},
contextTypes: {
appConfig: React.PropTypes.object,
appConfig: PropTypes.object,
},
/**
@ -294,7 +295,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody" ref="body">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
</a>
@ -326,7 +327,7 @@ module.exports = React.createClass({
}
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<div style={{display: "none"}}>
{ /*
* Add dummy copy of the "a" tag
@ -346,7 +347,7 @@ module.exports = React.createClass({
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName }
</a>
@ -359,7 +360,7 @@ module.exports = React.createClass({
} else {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
{ _t("Download %(text)s", { text: text }) }

View file

@ -17,8 +17,10 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ImageUtils from '../../../ImageUtils';
import Modal from '../../../Modal';
import sdk from '../../../index';
@ -28,28 +30,60 @@ import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'MImageBody',
export default class extends React.Component {
displayName: 'MImageBody'
propTypes: {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* called when the image has loaded */
onWidgetLoad: React.PropTypes.func.isRequired,
},
onWidgetLoad: PropTypes.func.isRequired,
}
getInitialState: function() {
return {
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
}
constructor(props) {
super(props);
this.onAction = this.onAction.bind(this);
this.onImageEnter = this.onImageEnter.bind(this);
this.onImageLeave = this.onImageLeave.bind(this);
this.onClientSync = this.onClientSync.bind(this);
this.onClick = this.onClick.bind(this);
this.fixupHeight = this.fixupHeight.bind(this);
this._isGif = this._isGif.bind(this);
this.state = {
decryptedUrl: null,
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
imgError: false,
};
},
}
componentWillMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
}
onClick: function onClick(ev) {
onClientSync(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING or PREPARED.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected && this.state.imgError) {
// Load the image again
this.setState({
imgError: false,
});
}
}
onClick(ev) {
if (ev.button == 0 && !ev.metaKey) {
ev.preventDefault();
const content = this.props.mxEvent.getContent();
@ -69,43 +103,49 @@ module.exports = React.createClass({
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}
},
}
_isGif: function() {
_isGif() {
const content = this.props.mxEvent.getContent();
return (
content &&
content.info &&
content.info.mimetype === "image/gif"
);
},
}
onImageEnter: function(e) {
onImageEnter(e) {
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
imgElement.src = this._getContentUrl();
},
}
onImageLeave: function(e) {
onImageLeave(e) {
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
imgElement.src = this._getThumbUrl();
},
}
_getContentUrl: function() {
onImageError() {
this.setState({
imgError: true,
});
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
return this.context.matrixClient.mxcUrlToHttp(content.url);
}
},
}
_getThumbUrl: function() {
_getThumbUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
// Don't use the thumbnail for clients wishing to autoplay gifs.
@ -114,11 +154,11 @@ module.exports = React.createClass({
}
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
}
},
}
componentDidMount: function() {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.fixupHeight();
const content = this.props.mxEvent.getContent();
@ -152,21 +192,35 @@ module.exports = React.createClass({
});
}).done();
}
},
this._afterComponentDidMount();
}
componentWillUnmount: function() {
// To be overridden by subclasses (e.g. MStickerBody) for further
// initialisation after componentDidMount
_afterComponentDidMount() {
}
componentWillUnmount() {
this.unmounted = true;
dis.unregister(this.dispatcherRef);
},
this.context.matrixClient.removeListener('sync', this.onClientSync);
this._afterComponentWillUnmount();
}
onAction: function(payload) {
// To be overridden by subclasses (e.g. MStickerBody) for further
// cleanup after componentWillUnmount
_afterComponentWillUnmount() {
}
onAction(payload) {
if (payload.action === "timeline_resize") {
this.fixupHeight();
}
},
}
fixupHeight: function() {
fixupHeight() {
if (!this.refs.image) {
console.warn("Refusing to fix up height on MImageBody with no image element");
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
return;
}
@ -182,10 +236,25 @@ module.exports = React.createClass({
}
this.refs.image.style.height = thumbHeight + "px";
// console.log("Image height now", thumbHeight);
},
}
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
_messageContent(contentUrl, thumbUrl, content) {
return (
<span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
);
}
render() {
const content = this.props.mxEvent.getContent();
if (this.state.error !== null) {
@ -216,6 +285,14 @@ module.exports = React.createClass({
);
}
if (this.state.imgError) {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
const contentUrl = this._getContentUrl();
let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
@ -225,18 +302,7 @@ module.exports = React.createClass({
}
if (thumbUrl) {
return (
<span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
);
return this._messageContent(contentUrl, thumbUrl, content);
} else if (content.body) {
return (
<span className="mx_MImageBody">
@ -250,5 +316,5 @@ module.exports = React.createClass({
</span>
);
}
},
});
}
}

View file

@ -0,0 +1,149 @@
/*
Copyright 2018 New Vector Ltd
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.
*/
'use strict';
import MImageBody from './MImageBody';
import sdk from '../../../index';
import TintableSVG from '../elements/TintableSvg';
export default class MStickerBody extends MImageBody {
displayName: 'MStickerBody'
constructor(props) {
super(props);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this._onImageLoad = this._onImageLoad.bind(this);
}
_onMouseEnter() {
this.setState({showTooltip: true});
}
_onMouseLeave() {
this.setState({showTooltip: false});
}
_onImageLoad() {
this.setState({
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
});
const hidePlaceholderTimer = setTimeout(() => {
this.setState({
placeholderVisible: false,
imageClasses: 'mx_MStickerBody_image_visible',
});
}, 500);
this.setState({hidePlaceholderTimer});
if (this.props.onWidgetLoad) {
this.props.onWidgetLoad();
}
}
_afterComponentDidMount() {
if (this.refs.image.complete) {
// Image already loaded
this.setState({
placeholderVisible: false,
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
imageClasses: 'mx_MStickerBody_image_visible',
});
} else {
// Image not already loaded
this.setState({
placeholderVisible: true,
placeholderClasses: '',
imageClasses: '',
});
}
}
_afterComponentWillUnmount() {
if (this.state.hidePlaceholderTimer) {
clearTimeout(this.state.hidePlaceholderTimer);
this.setState({hidePlaceholderTimer: null});
}
}
_messageContent(contentUrl, thumbUrl, content) {
let tooltip;
const tooltipBody = (
this.props.mxEvent &&
this.props.mxEvent.getContent() &&
this.props.mxEvent.getContent().body) ?
this.props.mxEvent.getContent().body : null;
if (this.state.showTooltip && tooltipBody) {
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
tooltip = <RoomTooltip
className='mx_RoleButton_tooltip'
label={tooltipBody} />;
}
const gutterSize = 0;
let placeholderSize = 75;
let placeholderFixupHeight = '100px';
let placeholderTop = 0;
let placeholderLeft = 0;
if (content.info) {
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
placeholderFixupHeight = content.info.h + 'px';
}
placeholderSize = placeholderSize + 'px';
// Body 'ref' required by MImageBody
return (
<span className='mx_MStickerBody' ref='body'
style={{
height: placeholderFixupHeight,
}}>
<div className={'mx_MStickerBody_image_container'}>
{ this.state.placeholderVisible &&
<div
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
style={{
top: placeholderTop,
left: placeholderLeft,
}}
>
<TintableSVG
src={'img/icons-show-stickers.svg'}
width={placeholderSize}
height={placeholderSize} />
</div> }
<img
className={'mx_MStickerBody_image ' + this.state.imageClasses}
src={contentUrl}
ref='image'
alt={content.body}
onLoad={this._onImageLoad}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
/>
{ tooltip }
</div>
</span>
);
}
// Empty to prevent default behaviour of MImageBody
onClick() {
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
@ -29,10 +30,10 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* called when the video has loaded */
onWidgetLoad: React.PropTypes.func.isRequired,
onWidgetLoad: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
module.exports = React.createClass({
@ -24,22 +25,22 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* a list of words to highlight */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* the shsape of the tile, used */
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
},
getEventTileOps: function() {
@ -64,15 +65,19 @@ module.exports = React.createClass({
let BodyType = UnknownBody;
if (msgtype && bodyTypes[msgtype]) {
BodyType = bodyTypes[msgtype];
} else if (this.props.mxEvent.getType() === 'm.sticker') {
BodyType = sdk.getComponent('messages.MStickerBody');
} else if (content.url) {
// Fallback to MFileBody if there's a content URL
BodyType = bodyTypes['m.file'];
}
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />;
return <BodyType
ref="body" mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />;
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
@ -27,7 +28,7 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
},
onAvatarClick: function(name) {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import * as linkify from 'linkifyjs';
@ -31,8 +33,6 @@ import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
import SettingsStore from "../../../settings/SettingsStore";
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
@ -43,19 +43,26 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* a list of words to highlight */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* callback for when our widget has loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
},
contextTypes: {
addRichQuote: PropTypes.func,
},
getInitialState: function() {
@ -179,6 +186,7 @@ module.exports = React.createClass({
// If the link is a (localised) matrix.to link, replace it with a pill
const Pill = sdk.getComponent('elements.Pill');
const Quote = sdk.getComponent('elements.Quote');
if (Pill.isMessagePillUrl(href)) {
const pillContainer = document.createElement('span');
@ -197,6 +205,21 @@ module.exports = React.createClass({
// update the current node with one that's now taken its place
node = pillContainer;
} else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
this.context.addRichQuote(href);
node.remove();
} else { // We're the first in the chain
const quoteContainer = document.createElement('span');
const quote =
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node);
node = quoteContainer;
}
pillified = true;
}
} else if (node.nodeType == Node.TEXT_NODE) {
const Pill = sdk.getComponent('elements.Pill');
@ -314,7 +337,7 @@ module.exports = React.createClass({
_addCodeCopyButton() {
// Add 'copy' buttons to pre blocks
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = (e) => {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const TextForEvent = require('../../../TextForEvent');
import sdk from '../../../index';
@ -26,7 +27,7 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
},
render: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
import Promise from 'bluebird';
const React = require('react');
import PropTypes from 'prop-types';
const ObjectUtils = require("../../../ObjectUtils");
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index");
@ -26,11 +27,11 @@ module.exports = React.createClass({
displayName: 'AliasSettings',
propTypes: {
roomId: React.PropTypes.string.isRequired,
canSetCanonicalAlias: React.PropTypes.bool.isRequired,
canSetAliases: React.PropTypes.bool.isRequired,
aliasEvents: React.PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: React.PropTypes.object, // MatrixEvent
roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired,
aliasEvents: PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: PropTypes.object, // MatrixEvent
},
getDefaultProps: function() {
@ -57,8 +58,8 @@ module.exports = React.createClass({
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
state.remoteDomains = Object.keys(state.domainToAliases).filter((alias) => {
return alias !== localDomain;
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0;
});
if (canonicalAliasEvent) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const Tinter = require('../../../Tinter');
@ -42,7 +43,7 @@ module.exports = React.createClass({
displayName: 'ColorSettings',
propTypes: {
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
},
getInitialState: function() {

View file

@ -15,10 +15,12 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual';
const GROUP_ID_REGEX = /\+\S+\:\S+/;
@ -26,13 +28,13 @@ module.exports = React.createClass({
displayName: 'RelatedGroupSettings',
propTypes: {
roomId: React.PropTypes.string.isRequired,
canSetRelatedGroups: React.PropTypes.bool.isRequired,
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
roomId: PropTypes.string.isRequired,
canSetRelatedGroups: PropTypes.bool.isRequired,
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient),
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
@ -43,13 +45,25 @@ module.exports = React.createClass({
getInitialState: function() {
return {
newGroupsList: this.props.relatedGroupsEvent ?
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
newGroupsList: this.getInitialGroupList(),
newGroupId: null,
};
},
getInitialGroupList: function() {
return this.props.relatedGroupsEvent ? (this.props.relatedGroupsEvent.getContent().groups || []) : [];
},
needsSaving: function() {
const cli = this.context.matrixClient;
const room = cli.getRoom(this.props.roomId);
if (!room.currentState.maySendStateEvent('m.room.related_groups', cli.getUserId())) return false;
return !isEqual(this.getInitialGroupList(), this.state.newGroupsList);
},
saveSettings: function() {
if (!this.needsSaving()) return Promise.resolve();
return this.context.matrixClient.sendStateEvent(
this.props.roomId,
'm.room.related_groups',
@ -97,7 +111,7 @@ module.exports = React.createClass({
onGroupDeleted: function(index) {
const newGroupsList = this.state.newGroupsList.slice();
newGroupsList.splice(index, 1),
newGroupsList.splice(index, 1);
this.setState({ newGroupsList });
},

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require("../../../index");
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'UrlPreviewSettings',
propTypes: {
room: React.PropTypes.object,
room: PropTypes.object,
},
saveSettings: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
@ -36,7 +37,15 @@ module.exports = React.createClass({
displayName: 'AppsDrawer',
propTypes: {
room: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
showApps: PropTypes.bool, // Should apps be rendered
hide: PropTypes.bool, // If rendered, should apps drawer be visible
},
defaultProps: {
showApps: true,
hide: false,
},
getInitialState: function() {
@ -47,7 +56,7 @@ module.exports = React.createClass({
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
},
componentDidMount: function() {
@ -57,7 +66,7 @@ module.exports = React.createClass({
this.scalarClient.connect().then(() => {
this.forceUpdate();
}).catch((e) => {
console.log("Failed to connect to integrations server");
console.log('Failed to connect to integrations server');
// TODO -- Handle Scalar errors
// this.setState({
// scalar_error: err,
@ -71,7 +80,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
ScalarMessaging.stopListening();
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
dis.unregister(this.dispatcherRef);
},
@ -82,7 +91,7 @@ module.exports = React.createClass({
},
onAction: function(action) {
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
case 'appsDrawer':
// When opening the app drawer when there aren't any apps,
@ -110,7 +119,7 @@ module.exports = React.createClass({
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { "$bar": "baz" }.
* variables with. E.g. { '$bar': 'baz' }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
encodeUri: function(pathTemplate, variables) {
@ -191,13 +200,13 @@ module.exports = React.createClass({
},
_launchManageIntegrations: function() {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
null;
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
}, 'mx_IntegrationsManager');
},
onClickAddWidget: function(e) {
@ -205,12 +214,12 @@ module.exports = React.createClass({
// Display a warning dialog if the max number of widgets have already been added to the room
const apps = this._getApps();
if (apps && apps.length >= MAX_WIDGETS) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
console.error(errorMsg);
Modal.createDialog(ErrorDialog, {
title: _t("Cannot add any more widgets"),
description: _t("The maximum permitted number of widgets have already been added to this room."),
title: _t('Cannot add any more widgets'),
description: _t('The maximum permitted number of widgets have already been added to this room.'),
});
return;
}
@ -242,11 +251,11 @@ module.exports = React.createClass({
) {
addWidget = <div
onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
role='button'
tabIndex='0'
className={this.state.apps.length<2 ?
"mx_AddWidget_button mx_AddWidget_button_full_width" :
"mx_AddWidget_button"
'mx_AddWidget_button mx_AddWidget_button_full_width' :
'mx_AddWidget_button'
}
title={_t('Add a widget')}>
[+] { _t('Add a widget') }
@ -254,8 +263,8 @@ module.exports = React.createClass({
}
return (
<div className="mx_AppsDrawer">
<div id="apps" className="mx_AppsContainer">
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
<div id='apps' className='mx_AppsContainer'>
{ apps }
</div>
{ this._canUserModify() && addWidget }

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
@ -29,26 +30,32 @@ module.exports = React.createClass({
propTypes: {
// js-sdk room object
room: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
showApps: React.PropTypes.bool,
room: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
showApps: PropTypes.bool, // Render apps
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
// Conference Handler implementation
conferenceHandler: React.PropTypes.object,
conferenceHandler: PropTypes.object,
// set to true to show the file drop target
draggingFile: React.PropTypes.bool,
draggingFile: PropTypes.bool,
// set to true to show the 'active conf call' banner
displayConfCallNotification: React.PropTypes.bool,
displayConfCallNotification: PropTypes.bool,
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: React.PropTypes.number,
maxHeight: PropTypes.number,
// a callback which is called when the content of the aux panel changes
// content in a way that is likely to make it change size.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
},
defaultProps: {
showApps: true,
hideAppsDrawer: false,
},
shouldComponentUpdate: function(nextProps, nextState) {
@ -133,6 +140,7 @@ module.exports = React.createClass({
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
hide={this.props.hideAppsDrawer}
/>;
return (

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,6 +18,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
@ -31,7 +33,11 @@ const PRESENCE_CLASS = {
};
function presenceClassForMember(presenceState, lastActiveAgo) {
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
if (showPresence === false) {
return 'mx_EntityTile_online_beenactive';
}
// offline is split into two categories depending on whether we have
// a last_active_ago for them.
if (presenceState == 'offline') {
@ -51,18 +57,19 @@ const EntityTile = React.createClass({
displayName: 'EntityTile',
propTypes: {
name: React.PropTypes.string,
title: React.PropTypes.string,
avatarJsx: React.PropTypes.any, // <BaseAvatar />
className: React.PropTypes.string,
presenceState: React.PropTypes.string,
presenceLastActiveAgo: React.PropTypes.number,
presenceLastTs: React.PropTypes.number,
presenceCurrentlyActive: React.PropTypes.bool,
showInviteButton: React.PropTypes.bool,
shouldComponentUpdate: React.PropTypes.func,
onClick: React.PropTypes.func,
suppressOnHover: React.PropTypes.bool,
name: PropTypes.string,
title: PropTypes.string,
avatarJsx: PropTypes.any, // <BaseAvatar />
className: PropTypes.string,
presenceState: PropTypes.string,
presenceLastActiveAgo: PropTypes.number,
presenceLastTs: PropTypes.number,
presenceCurrentlyActive: PropTypes.bool,
showInviteButton: PropTypes.bool,
shouldComponentUpdate: PropTypes.func,
onClick: PropTypes.func,
suppressOnHover: PropTypes.bool,
showPresence: PropTypes.bool,
},
getDefaultProps: function() {
@ -74,6 +81,7 @@ const EntityTile = React.createClass({
presenceLastTs: 0,
showInviteButton: false,
suppressOnHover: false,
showPresence: true,
};
},
@ -98,7 +106,7 @@ const EntityTile = React.createClass({
render: function() {
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo,
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
);
let mainClassName = "mx_EntityTile ";
@ -113,15 +121,21 @@ const EntityTile = React.createClass({
mainClassName += " mx_EntityTile_hover";
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
let presenceLabel = null;
let nameClasses = 'mx_EntityTile_name';
if (this.props.showPresence) {
presenceLabel = <PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />;
nameClasses += ' mx_EntityTile_name_hover';
}
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12" />
<EmojiText element="div" className="mx_EntityTile_name mx_EntityTile_name_hover" dir="auto">
<EmojiText element="div" className={nameClasses} dir="auto">
{ name }
</EmojiText>
<PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
{presenceLabel}
</div>
);
} else {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
const React = require('react');
import PropTypes from 'prop-types';
const classNames = require("classnames");
import { _t, _td } from '../../../languageHandler';
const Modal = require('../../../Modal');
@ -28,11 +30,13 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
import {makeEventPermalink} from "../../../matrix-to";
const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.sticker': 'messages.MessageEvent',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
@ -75,65 +79,65 @@ module.exports = withMatrixClient(React.createClass({
propTypes: {
/* MatrixClient instance for sender verification etc */
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
* references the same this.props.mxEvent.
*/
isRedacted: React.PropTypes.bool,
isRedacted: PropTypes.bool,
/* true if this is a continuation of the previous event (which has the
* effect of not showing another avatar/displayname
*/
continuation: React.PropTypes.bool,
continuation: PropTypes.bool,
/* true if this is the last event in the timeline (which has the effect
* of always showing the timestamp)
*/
last: React.PropTypes.bool,
last: PropTypes.bool,
/* true if this is search context (which has the effect of greying out
* the text
*/
contextual: React.PropTypes.bool,
contextual: PropTypes.bool,
/* a list of words to highlight, ordered by longest first */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* is this the focused event */
isSelectedEvent: React.PropTypes.bool,
isSelectedEvent: PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
readReceipts: PropTypes.arrayOf(React.PropTypes.object),
/* opaque readreceipt info for each userId; used by ReadReceiptMarker
* to manage its animations. Should be an empty object when the room
* first loads
*/
readReceiptMap: React.PropTypes.object,
readReceiptMap: PropTypes.object,
/* A function which is used to check if the parent panel is being
* unmounted, to avoid unnecessary work. Should return true if we
* are being unmounted.
*/
checkUnmounting: React.PropTypes.func,
checkUnmounting: PropTypes.func,
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
eventSendStatus: PropTypes.string,
/* the shape of the tile. by default, the layout is intended for the
* normal room timeline. alternative values are: "file_list", "file_grid"
@ -142,14 +146,24 @@ module.exports = withMatrixClient(React.createClass({
* boiilerplatey. So just make the necessary render decisions conditional
* for now.
*/
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
isTwelveHour: PropTypes.bool,
},
getInitialState: function() {
return {menu: false, allReadAvatars: false, verified: null};
return {
// Whether the context menu is being displayed.
menu: false,
// Whether all read receipts are being displayed. If not, only display
// a truncation of them.
allReadAvatars: false,
// Whether the event's sender has been verified.
verified: null,
// Whether onRequestKeysClick has been called since mounting.
previouslyRequestedKeys: false,
};
},
componentWillMount: function() {
@ -390,6 +404,19 @@ module.exports = withMatrixClient(React.createClass({
});
},
onRequestKeysClick: function() {
this.setState({
// Indicate in the UI that the keys have been requested (this is expected to
// be reset if the component is mounted in the future).
previouslyRequestedKeys: true,
});
// 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.props.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
},
onPermalinkClicked: function(e) {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
@ -444,7 +471,7 @@ module.exports = withMatrixClient(React.createClass({
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
const isInfoMessage = (eventType !== 'm.room.message');
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type
@ -455,6 +482,7 @@ module.exports = withMatrixClient(React.createClass({
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
const classes = classNames({
mx_EventTile: true,
@ -471,14 +499,12 @@ module.exports = withMatrixClient(React.createClass({
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
const permalink = "https://matrix.to/#/" +
this.props.mxEvent.getRoomId() + "/" +
this.props.mxEvent.getId();
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
const readAvatars = this.getReadAvatars();
@ -516,7 +542,7 @@ module.exports = withMatrixClient(React.createClass({
if (needsSenderProfile) {
let text = null;
if (!this.props.tileShape) {
if (!this.props.tileShape || this.props.tileShape === 'quote') {
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
@ -533,79 +559,139 @@ module.exports = withMatrixClient(React.createClass({
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") {
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</div>
const keyRequestHelpText =
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
<p>
{ this.state.previouslyRequestedKeys ?
_t( 'Your key share request has been sent - please check your other devices ' +
'for key share requests.') :
_t( 'Key share requests are sent to your other devices automatically. If you ' +
'rejected or dismissed the key share request on your other devices, click ' +
'here to request the keys for this session again.')
}
</p>
<p>
{ _t( 'If your other devices do not have the key for this message you will not ' +
'be able to decrypt them.')
}
</p>
</div>;
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
_t('Key request sent.') :
_t(
'<requestLink>Re-request encryption keys</requestLink> from your other devices.',
{},
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
);
} else if (this.props.tileShape === "file_grid") {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a
className="mx_EventTile_senderDetailsLink"
href={permalink}
onClick={this.onPermalinkClicked}
>
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
const keyRequestInfo = isEncryptionFailure ?
<div className="mx_EventTile_keyRequestInfo">
<span className="mx_EventTile_keyRequestInfo_text">
{ keyRequestInfoContent }
</span>
<ToolTipButton helpText={keyRequestHelpText} />
</div> : null;
switch (this.props.tileShape) {
case 'notif': {
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</a>
</div>
);
} else {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
);
}
case 'file_grid': {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a
className="mx_EventTile_senderDetailsLink"
href={permalink}
onClick={this.onPermalinkClicked}
>
<div className="mx_EventTile_senderDetails">
{ sender }
{ timestamp }
</div>
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ editButton }
</div>
</div>
);
);
}
case 'quote': {
return (
<div className={classes}>
{ avatar }
{ sender }
<div className="mx_EventTile_line mx_EventTile_quote">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
tileShape="quote"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onWidgetLoad={this.props.onWidgetLoad}
showUrlPreview={false} />
</div>
</div>
);
}
default: {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ keyRequestInfo }
{ editButton }
</div>
</div>
);
}
}
},
}));
@ -658,3 +744,5 @@ function E2ePadlockUnencrypted(props) {
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
}
module.exports.getHandlerTile = getHandlerTile;

View file

@ -16,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import { KeyCode } from '../../../Keyboard';
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
onCancelClick: React.PropTypes.func.isRequired,
onCancelClick: PropTypes.func.isRequired,
},
componentWillMount: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -32,10 +33,10 @@ module.exports = React.createClass({
displayName: 'LinkPreviewWidget',
propTypes: {
link: React.PropTypes.string.isRequired, // the URL being previewed
mxEvent: React.PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: React.PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: React.PropTypes.func, // called when the preview's contents has loaded
link: PropTypes.string.isRequired, // the URL being previewed
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: PropTypes.func, // called when the preview's contents has loaded
},
getInitialState: function() {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -65,6 +66,6 @@ export default class MemberDeviceInfo extends React.Component {
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -27,6 +27,7 @@ limitations under the License.
* 'isTargetMod': boolean
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
@ -38,16 +39,15 @@ import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
import SdkConfig from '../../../SdkConfig';
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
member: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
},
getInitialState: function() {
@ -440,40 +440,57 @@ module.exports = withMatrixClient(React.createClass({
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
const self = this;
if (!room) {
return;
}
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
if (!powerLevelEvent) {
return;
}
if (powerLevelEvent.getContent().users) {
const myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
if (parseInt(myPower) === parseInt(powerLevel)) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
} else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
} else {
if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
if (!powerLevelEvent.getContent().users) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
return;
}
const myUserId = this.props.matrixClient.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) {
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: (confirmed) => {
if (confirmed) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
return;
}
const myPower = powerLevelEvent.getContent().users[myUserId];
if (parseInt(myPower) === parseInt(powerLevel)) {
Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: (confirmed) => {
if (confirmed) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
return;
}
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
},
onNewDMClick: function() {
@ -713,6 +730,10 @@ module.exports = withMatrixClient(React.createClass({
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
const dmRoomMap = new DMRoomMap(this.props.matrixClient);
// 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
// especially as logic below concerns specially if we haven't joined but have been invited
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
const RoomTile = sdk.getComponent("rooms.RoomTile");
@ -722,12 +743,18 @@ module.exports = withMatrixClient(React.createClass({
const room = this.props.matrixClient.getRoom(roomId);
if (room) {
const me = room.getMember(this.props.matrixClient.credentials.userId);
const highlight = (
room.getUnreadNotificationCount('highlight') > 0 ||
me.membership === "invite"
);
// not a DM room if we have are not joined
if (!me.membership || me.membership !== 'join') continue;
// not a DM room if they are not joined
const them = this.props.member;
if (!them.membership || them.membership !== 'join') continue;
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
tiles.push(
<RoomTile key={room.roomId} room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
@ -831,13 +858,27 @@ module.exports = withMatrixClient(React.createClass({
}
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
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.props.matrixClient.baseUrl;
let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[hsUrl];
}
let presenceLabel = null;
if (showPresence) {
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />;
}
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
roomMemberDetails = <div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b>
@ -850,18 +891,17 @@ module.exports = withMatrixClient(React.createClass({
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
{presenceLabel}
</div>
</div>;
}
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
@ -885,7 +925,7 @@ module.exports = withMatrixClient(React.createClass({
{ this._renderDevices() }
{ spinner }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,9 +18,9 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
const MatrixClientPeg = require("../../../MatrixClientPeg");
const sdk = require('../../../index');
const GeminiScrollbar = require('react-gemini-scrollbar');
const rate_limited_func = require('../../../ratelimitedfunc');
const CallHandler = require("../../../CallHandler");
@ -59,6 +59,14 @@ module.exports = React.createClass({
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl;
this._showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
},
componentWillUnmount: function() {
@ -315,13 +323,37 @@ module.exports = React.createClass({
});
},
_getPending3PidInvites: function() {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < requiredKeys.length; ++i) {
if (e.getContent()[requiredKeys[i]] === undefined) return false;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) return false;
return true;
});
}
},
_makeMemberTiles: function(members, membership) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
const memberList = members.map((userId) => {
const m = this.memberDict[userId];
return (
<MemberTile key={userId} member={m} ref={userId} />
<MemberTile key={userId} member={m} ref={userId} showPresence={this._showPresence} />
);
});
@ -329,33 +361,16 @@ module.exports = React.createClass({
// Double XXX: Now it's really, really not the right home for this logic:
// we shouldn't even be passing in the 'membership' param to this function.
// Ew, ew, and ew.
// Triple XXX: This violates the size constraint, the output is expected/desired
// to be the same length as the members input array.
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const EntityTile = sdk.getComponent("rooms.EntityTile");
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const required_keys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < required_keys.length; ++i) {
if (e.getContent()[required_keys[i]] === undefined) return;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />,
);
});
}
memberList.push(...this._getPending3PidInvites().map((e) => {
return <EntityTile key={e.getStateKey()}
name={e.getContent().display_name}
suppressOnHover={true}
/>;
}));
}
return memberList;
@ -374,11 +389,12 @@ module.exports = React.createClass({
},
_getChildCountInvited: function() {
return this.state.filteredInvitedMembers.length;
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
},
render: function() {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
let invitedSection = null;
if (this._getChildCountInvited() > 0) {
@ -407,14 +423,14 @@ module.exports = React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined}
/>
{ invitedSection }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
@ -28,7 +29,14 @@ module.exports = React.createClass({
displayName: 'MemberTile',
propTypes: {
member: React.PropTypes.any.isRequired, // RoomMember
member: PropTypes.any.isRequired, // RoomMember
showPresence: PropTypes.bool,
},
getDefaultProps: function() {
return {
showPresence: true,
};
},
getInitialState: function() {
@ -98,7 +106,7 @@ module.exports = React.createClass({
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerStatus={powerStatus} />
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} />
);
},
});

View file

@ -15,15 +15,16 @@ 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 CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
export default class MessageComposer extends React.Component {
constructor(props, context) {
@ -31,8 +32,6 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onShowAppsClick = this.onShowAppsClick.bind(this);
this.onHideAppsClick = this.onHideAppsClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
@ -42,6 +41,7 @@ export default class MessageComposer extends React.Component {
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this.state = {
autocompleteQuery: '',
@ -53,6 +53,7 @@ export default class MessageComposer extends React.Component {
wordCount: 0,
},
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
};
}
@ -62,12 +63,16 @@ export default class MessageComposer extends React.Component {
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
}
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
}
onEvent(event) {
@ -76,6 +81,12 @@ export default class MessageComposer extends React.Component {
this.forceUpdate();
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
}
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
@ -189,20 +200,6 @@ export default class MessageComposer extends React.Component {
// this._startCallApp(true);
}
onShowAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: true,
});
}
onHideAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({
autocompleteQuery: content,
@ -268,7 +265,12 @@ export default class MessageComposer extends React.Component {
alt={e2eTitle} title={e2eTitle}
/>,
);
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
let callButton;
let videoCallButton;
let hangupButton;
// Call buttons
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
@ -285,19 +287,6 @@ export default class MessageComposer extends React.Component {
</div>;
}
// Apps
if (this.props.showApps) {
hideAppsButton =
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
<TintableSvg src="img/icons-hide-apps.svg" width="35" height="35" />
</div>;
} else {
showAppsButton =
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
<TintableSvg src="img/icons-show-apps.svg" width="35" height="35" />
</div>;
}
const canSendMessages = this.props.room.currentState.maySendMessage(
MatrixClientPeg.get().credentials.userId);
@ -325,8 +314,25 @@ export default class MessageComposer extends React.Component {
key="controls_formatting" />
);
const placeholderText = roomIsEncrypted ?
_t('Send an encrypted message') + '…' : _t('Send a message (unencrypted)') + '…';
let placeholderText;
if (this.state.isQuoting) {
if (roomIsEncrypted) {
placeholderText = _t('Send an encrypted reply…');
} else {
placeholderText = _t('Send a reply (unencrypted)…');
}
} else {
if (roomIsEncrypted) {
placeholderText = _t('Send an encrypted message…');
} else {
placeholderText = _t('Send a message (unencrypted)…');
}
}
let stickerpickerButton;
if (SettingsStore.isFeatureEnabled('feature_sticker_messages')) {
stickerpickerButton = <Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />;
}
controls.push(
<MessageComposerInput
@ -339,12 +345,11 @@ export default class MessageComposer extends React.Component {
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
formattingButton,
stickerpickerButton,
uploadButton,
hangupButton,
callButton,
videoCallButton,
showAppsButton,
hideAppsButton,
);
} else {
controls.push(
@ -399,17 +404,17 @@ export default class MessageComposer extends React.Component {
MessageComposer.propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
callState: PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
uploadFile: PropTypes.func.isRequired,
// string representing the current room app drawer state
showApps: React.PropTypes.bool,
showApps: PropTypes.bool,
};

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
@ -50,6 +51,10 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
import QuotePreview from "./QuotePreview";
import RoomViewStore from '../../../stores/RoomViewStore';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@ -86,15 +91,15 @@ export default class MessageComposerInput extends React.Component {
static propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onContentChanged: PropTypes.func,
onInputStateChanged: React.PropTypes.func,
onInputStateChanged: PropTypes.func,
};
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
@ -268,6 +273,7 @@ export default class MessageComposerInput extends React.Component {
let contentState = this.state.editorState.getCurrentContent();
switch (payload.action) {
case 'quote_event':
case 'focus_composer':
editor.focus();
break;
@ -281,12 +287,13 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion({
completion,
selection,
href: `https://matrix.to/#/${payload.user_id}`,
href: makeUserPermalink(payload.user_id),
suffix: selection.getStartOffset() === 0 ? ': ' : ' ',
});
}
break;
case 'quote': {
case 'quote': { // old quoting, whilst rich quoting is in labs
/// XXX: Not doing rich-text quoting from formatted-body because draft-js
/// has regressed such that when links are quoted, errors are thrown. See
/// https://github.com/vector-im/riot-web/issues/4756.
@ -512,7 +519,8 @@ export default class MessageComposerInput extends React.Component {
// composer. For some reason the editor won't scroll automatically if we paste
// blocks of text in or insert newlines.
if (textContent.slice(selection.start).indexOf("\n") === -1) {
this.refs.editor.refs.editor.scrollTop = this.refs.editor.refs.editor.scrollHeight;
let editorRoot = this.refs.editor.refs.editor.parentNode.parentNode;
editorRoot.scrollTop = editorRoot.scrollHeight;
}
});
}
@ -652,7 +660,7 @@ export default class MessageComposerInput extends React.Component {
}
return false;
}
};
onTextPasted(text: string, html?: string) {
const currentSelection = this.state.editorState.getSelection();
@ -715,6 +723,7 @@ export default class MessageComposerInput extends React.Component {
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
if (cmd) {
if (!cmd.error) {
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
this.setState({
editorState: this.createEditorState(),
});
@ -742,9 +751,17 @@ export default class MessageComposerInput extends React.Component {
return true;
}
const quotingEv = RoomViewStore.getQuotingEvent();
if (this.state.isRichtextEnabled) {
// We should only send HTML if any block is styled or contains inline style
let shouldSendHTML = false;
// If we are quoting we need HTML Content
if (quotingEv) {
shouldSendHTML = true;
}
const blocks = contentState.getBlocksAsArray();
if (blocks.some((block) => block.getType() !== 'unstyled')) {
shouldSendHTML = true;
@ -802,7 +819,8 @@ export default class MessageComposerInput extends React.Component {
}).join('\n');
const md = new Markdown(pt);
if (md.isPlainText()) {
// if contains no HTML and we're not quoting (needing HTML)
if (md.isPlainText() && !quotingEv) {
contentText = md.toPlaintext();
} else {
contentHTML = md.toHTML();
@ -825,6 +843,24 @@ export default class MessageComposerInput extends React.Component {
sendTextFn = this.client.sendEmoteMessage;
}
if (quotingEv) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(quotingEv.getRoomId());
const sender = room.currentState.getMember(quotingEv.getSender());
const {body/*, formatted_body*/} = quotingEv.getContent();
const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId());
contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
// we have finished quoting, clear the quotingEvent
dis.dispatch({
action: 'quote_event',
event: null,
});
}
let sendMessagePromise;
if (contentHTML) {
sendMessagePromise = sendHtmlFn.call(
@ -1137,6 +1173,7 @@ export default class MessageComposerInput extends React.Component {
return (
<div className="mx_MessageComposer_input_wrapper">
<div className="mx_MessageComposer_autocomplete_wrapper">
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <QuotePreview /> }
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}
@ -1177,15 +1214,15 @@ export default class MessageComposerInput extends React.Component {
MessageComposerInput.propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onContentChanged: PropTypes.func,
onFilesPasted: React.PropTypes.func,
onFilesPasted: PropTypes.func,
onInputStateChanged: React.PropTypes.func,
onInputStateChanged: PropTypes.func,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
@ -25,9 +26,9 @@ import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PinnedEventTile',
propTypes: {
mxRoom: React.PropTypes.object.isRequired,
mxEvent: React.PropTypes.object.isRequired,
onUnpinned: React.PropTypes.func,
mxRoom: PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
onUnpinned: PropTypes.func,
},
onTileClicked: function() {
dis.dispatch({

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
@ -25,9 +26,9 @@ module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
propTypes: {
// The Room from the js-sdk we're going to show pinned events for
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func,
onCancelClick: PropTypes.func,
},
getInitialState: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@ -27,14 +28,14 @@ module.exports = React.createClass({
propTypes: {
// number of milliseconds ago this user was last active.
// zero = unknown
activeAgo: React.PropTypes.number,
activeAgo: PropTypes.number,
// if true, activeAgo is an approximation and "Now" should
// be shown instead
currentlyActive: React.PropTypes.bool,
currentlyActive: PropTypes.bool,
// offline, online, etc
presenceState: React.PropTypes.string,
presenceState: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -0,0 +1,78 @@
/*
Copyright 2017 New Vector Ltd
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 dis from '../../../dispatcher';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore';
function cancelQuoting() {
dis.dispatch({
action: 'quote_event',
event: null,
});
}
export default class QuotePreview extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
event: null,
};
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate();
}
componentWillUnmount() {
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
}
_onRoomViewStoreUpdate() {
const event = RoomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });
}
}
render() {
if (!this.state.event) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
return <div className="mx_QuotePreview">
<div className="mx_QuotePreview_section">
<EmojiText element="div" className="mx_QuotePreview_header mx_QuotePreview_title">
{ '💬 ' + _t('Replying') }
</EmojiText>
<div className="mx_QuotePreview_header mx_QuotePreview_cancel">
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
onClick={cancelQuoting} />
</div>
<div className="mx_QuotePreview_clear" />
<EventTile mxEvent={this.state.event} last={true} tileShape="quote" />
</div>
</div>;
}
}

View file

@ -18,6 +18,7 @@ limitations under the License.
const React = require('react');
const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
@ -25,7 +26,7 @@ const Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
import { _t } from '../../../languageHandler';
import DateUtils from '../../../DateUtils';
import {formatDate} from '../../../DateUtils';
let bounce = false;
try {
@ -40,35 +41,35 @@ module.exports = React.createClass({
propTypes: {
// the RoomMember to show the RR for
member: React.PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
// number of pixels to offset the avatar from the right of its parent;
// typically a negative value.
leftOffset: React.PropTypes.number,
leftOffset: PropTypes.number,
// true to hide the avatar (it will still be animated)
hidden: React.PropTypes.bool,
hidden: PropTypes.bool,
// don't animate this RR into position
suppressAnimation: React.PropTypes.bool,
suppressAnimation: PropTypes.bool,
// an opaque object for storing information about this user's RR in
// this room
readReceiptInfo: React.PropTypes.object,
readReceiptInfo: PropTypes.object,
// A function which is used to check if the parent panel is being
// unmounted, to avoid unnecessary work. Should return true if we
// are being unmounted.
checkUnmounting: React.PropTypes.func,
checkUnmounting: PropTypes.func,
// callback for clicks on this RR
onClick: React.PropTypes.func,
onClick: PropTypes.func,
// Timestamp when the receipt was read
timestamp: React.PropTypes.number,
timestamp: PropTypes.number,
// True to show twelve hour format, false otherwise
showTwelveHour: React.PropTypes.bool,
showTwelveHour: PropTypes.bool,
},
getDefaultProps: function() {
@ -184,10 +185,21 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)},
);
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
if (this.props.member.userId === this.props.member.rawDisplayName) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId,
dateTime: dateString},
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{displayName: this.props.member.rawDisplayName,
userName: this.props.member.userId,
dateTime: dateString},
);
}
}
return (

View file

@ -18,121 +18,40 @@ import sdk from '../../../index';
import dis from '../../../dispatcher';
import React from 'react';
import { _t } from '../../../languageHandler';
import linkifyString from 'linkifyjs/string';
import sanitizeHtml from 'sanitize-html';
import { ContentRepo } from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import classNames from 'classnames';
function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
}
const RoomDetailRow = React.createClass({
propTypes: {
room: PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
}),
},
onClick: function(ev) {
ev.preventDefault();
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId,
room_alias: this.props.room.canonicalAlias || (this.props.room.aliases || [])[0],
});
},
onTopicClick: function(ev) {
// When clicking a link in the topic, prevent the event being propagated
// to `onClick`.
ev.stopPropagation();
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const room = this.props.room;
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
const topic = linkifyString(sanitizeHtml(room.topic || ''));
const guestRead = room.worldReadable ? (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
) : <div />;
const guestJoin = room.guestCanJoin ? (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
) : <div />;
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
{ guestRead }
{ guestJoin }
</div>) : <div />;
return <tr key={room.roomId} onClick={this.onClick}>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={name} idName={name}
url={ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatarUrl, 24, 24, "crop")} />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic"
onClick={this.onTopicClick}
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ room.numJoinedMembers }
</td>
</tr>;
},
});
import {roomShape} from './RoomDetailRow';
export default React.createClass({
displayName: 'RoomDetailList',
propTypes: {
rooms: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
})),
rooms: PropTypes.arrayOf(roomShape),
className: PropTypes.string,
},
getRows: function() {
if (!this.props.rooms) return [];
const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow');
return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} />;
return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />;
});
},
onDetailsClick: function(ev, room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
room_alias: room.canonicalAlias || (room.aliases || [])[0],
});
},
render() {
const rows = this.getRows();
let rooms;
if (rows.length == 0) {
if (rows.length === 0) {
rooms = <i>{ _t('No rooms to show') }</i>;
} else {
rooms = <table ref="directory_table" className="mx_RoomDirectory_table">

View file

@ -0,0 +1,120 @@
/*
Copyright 2017 New Vector Ltd.
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 sdk from '../../../index';
import React from 'react';
import { _t } from '../../../languageHandler';
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import { ContentRepo } from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';
linkifyMatrix(linkify);
export function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
}
export const roomShape = PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
});
export default React.createClass({
propTypes: {
room: roomShape,
// passes ev, room as args
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
},
_linkifyTopic: function() {
if (this.refs.topic) {
linkifyElement(this.refs.topic, linkifyMatrix.options);
}
},
componentDidMount: function() {
this._linkifyTopic();
},
componentDidUpdate: function() {
this._linkifyTopic();
},
onClick: function(ev) {
ev.preventDefault();
if (this.props.onClick) {
this.props.onClick(ev, this.props.room);
}
},
onTopicClick: function(ev) {
// When clicking a link in the topic, prevent the event being propagated
// to `onClick`.
ev.stopPropagation();
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const room = this.props.room;
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
const guestRead = room.worldReadable ? (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
) : <div />;
const guestJoin = room.guestCanJoin ? (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
) : <div />;
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
{ guestRead }&nbsp;
{ guestJoin }
</div>) : <div />;
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={name} idName={name}
url={ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatarUrl, 24, 24, "crop")} />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic" ref="topic" onClick={this.onTopicClick}>
{ room.topic }
</div>
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ room.numJoinedMembers }
</td>
</tr>;
},
});

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -39,18 +40,18 @@ module.exports = React.createClass({
displayName: 'RoomHeader',
propTypes: {
room: React.PropTypes.object,
oobData: React.PropTypes.object,
editing: React.PropTypes.bool,
saving: React.PropTypes.bool,
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onPinnedClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func,
onLeaveClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
room: PropTypes.object,
oobData: PropTypes.object,
editing: PropTypes.bool,
saving: PropTypes.bool,
inRoom: PropTypes.bool,
collapsedRhs: PropTypes.bool,
onSettingsClick: PropTypes.func,
onPinnedClick: PropTypes.func,
onSaveClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
},
getDefaultProps: function() {
@ -391,7 +392,7 @@ module.exports = React.createClass({
let manageIntegsButton;
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton
roomId={this.props.room.roomId}
room={this.props.room}
/>;
}

View file

@ -18,20 +18,22 @@ limitations under the License.
'use strict';
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar');
const MatrixClientPeg = require("../../../MatrixClientPeg");
const CallHandler = require('../../../CallHandler');
const dis = require("../../../dispatcher");
const sdk = require('../../../index');
const rate_limited_func = require('../../../ratelimitedfunc');
const Rooms = require('../../../Rooms');
import * as Rooms from '../../../Rooms';
import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt');
import FilterStore from '../../../stores/FilterStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import RoomListStore from '../../../stores/RoomListStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
function phraseForSection(section) {
switch (section) {
@ -52,9 +54,9 @@ module.exports = React.createClass({
displayName: 'RoomList',
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
searchFilter: React.PropTypes.string,
ConferenceHandler: PropTypes.any,
collapsed: PropTypes.bool.isRequired,
searchFilter: PropTypes.string,
},
getInitialState: function() {
@ -74,11 +76,7 @@ module.exports = React.createClass({
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Event.decrypted", this.onEventDecrypted);
cli.on("accountData", this.onAccountData);
@ -86,30 +84,38 @@ module.exports = React.createClass({
const dmRoomMap = DMRoomMap.shared();
this._groupStores = {};
this._groupStoreTokens = [];
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
// $groupId: [$roomId1, $roomId2, ...],
};
// All rooms that should be kept in the room list when filtering
this._visibleRooms = [];
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getRooms();
// When the selected tags are changed, initialise a group store if necessary
this._filterStoreToken = FilterStore.addListener(() => {
FilterStore.getSelectedTags().forEach((tag) => {
this._tagStoreToken = TagOrderStore.addListener(() => {
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
this._groupStores[tag].registerListener(() => {
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
});
this._groupStoreTokens.push(
this._groupStores[tag].registerListener(() => {
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
}),
);
});
// Filters themselves have changed, refresh the selected tags
this.updateVisibleRooms();
});
this._roomListStoreToken = RoomListStore.addListener(() => {
this._delayedRefreshRoomList();
});
this.refreshRoomList();
// order of the sublists
@ -152,12 +158,6 @@ module.exports = React.createClass({
});
}
break;
case 'on_room_read':
// Force an update because the notif count state is too deep to cause
// an update. This forces the local echo of reading notifs to be
// reflected by the RoomTiles.
this.forceUpdate();
break;
}
},
@ -168,19 +168,24 @@ module.exports = React.createClass({
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}
if (this._filterStoreToken) {
this._filterStoreToken.remove();
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
if (this._roomListStoreToken) {
this._roomListStoreToken.remove();
}
if (this._groupStoreTokens.length > 0) {
// NB: GroupStore is not a Flux.Store
this._groupStoreTokens.forEach((token) => token.unregister());
}
// cancel any pending calls to the rate_limited_funcs
@ -188,11 +193,11 @@ module.exports = React.createClass({
},
onRoom: function(room) {
this._delayedRefreshRoomList();
this.updateVisibleRooms();
},
onDeleteRoom: function(roomId) {
this._delayedRefreshRoomList();
this.updateVisibleRooms();
},
onArchivedHeaderClick: function(isHidden, scrollToPosition) {
@ -219,13 +224,6 @@ module.exports = React.createClass({
this._updateStickyHeaders(true, scrollToPosition);
},
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
},
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
@ -234,18 +232,6 @@ module.exports = React.createClass({
}
},
onRoomName: function(room) {
this._delayedRefreshRoomList();
},
onRoomTags: function(event, room) {
this._delayedRefreshRoomList();
},
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
},
onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList();
},
@ -289,23 +275,32 @@ module.exports = React.createClass({
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
this._visibleRooms = [];
FilterStore.getSelectedTags().forEach((tag) => {
const selectedTags = TagOrderStore.getSelectedTags();
const visibleGroupRooms = [];
selectedTags.forEach((tag) => {
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => this._visibleRooms.push(roomId),
(roomId) => visibleGroupRooms.push(roomId),
);
});
this.setState({
selectedTags: FilterStore.getSelectedTags(),
}, () => {
this.refreshRoomList();
});
},
isRoomInSelectedTags: function(room) {
// No selected tags = every room is visible in the list
return this.state.selectedTags.length === 0 || this._visibleRooms.includes(room.roomId);
// If there are any tags selected, constrain the rooms listed to the
// visible rooms as determined by visibleGroupRooms. Here, we
// de-duplicate and filter out rooms that the client doesn't know
// about (hence the Set and the null-guard on `room`).
if (selectedTags.length > 0) {
const roomSet = new Set();
visibleGroupRooms.forEach((roomId) => {
const room = MatrixClientPeg.get().getRoom(roomId);
if (room) {
roomSet.add(room);
}
});
this._visibleRooms = Array.from(roomSet);
} else {
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getRooms();
}
this._delayedRefreshRoomList();
},
refreshRoomList: function() {
@ -319,83 +314,49 @@ module.exports = React.createClass({
totalRooms += l.length;
}
this.setState({
lists: this.getRoomLists(),
lists,
totalRoomCount: totalRooms,
// Do this here so as to not render every time the selected tags
// themselves change.
selectedTags: TagOrderStore.getSelectedTags(),
});
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
const lists = {};
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
lists["im.vector.fake.direct"] = [];
lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = [];
const lists = RoomListStore.getRoomLists();
const dmRoomMap = DMRoomMap.shared();
MatrixClientPeg.get().getRooms().forEach((room) => {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
const filteredLists = {};
// console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership);
const isRoomVisible = {
// $roomId: true,
};
if (me.membership == "invite") {
lists["im.vector.fake.invite"].push(room);
} else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
} else if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
// Used to split rooms via tags
const tagNames = Object.keys(room.tags);
this._visibleRooms.forEach((r) => {
isRoomVisible[r.roomId] = true;
});
// Apply TagPanel filtering, derived from FilterStore
if (!this.isRoomInSelectedTags(room)) {
Object.keys(lists).forEach((tagName) => {
const filteredRooms = lists[tagName].filter((taggedRoom) => {
// Somewhat impossible, but guard against it anyway
if (!taggedRoom) {
return;
}
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
return;
}
if (tagNames.length) {
for (let i = 0; i < tagNames.length; i++) {
const tagName = tagNames[i];
lists[tagName] = lists[tagName] || [];
lists[tagName].push(room);
}
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
lists["im.vector.fake.direct"].push(room);
} else {
lists["im.vector.fake.recent"].push(room);
}
} else if (me.membership === "leave") {
lists["im.vector.fake.archived"].push(room);
} else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
return Boolean(isRoomVisible[taggedRoom.roomId]);
});
if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) {
filteredLists[tagName] = filteredRooms;
}
});
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return lists;
return filteredLists;
},
_getScrollNode: function() {
@ -546,7 +507,8 @@ module.exports = React.createClass({
onShowMoreRooms: function() {
// kick gemini in the balls to get it to wake up
// XXX: uuuuuuugh.
this.refs.gemscroll.forceUpdate();
if (!this._gemScroll) return;
this._gemScroll.forceUpdate();
},
_getEmptyContent: function(section) {
@ -564,17 +526,20 @@ module.exports = React.createClass({
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
let tip = null;
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
tip = <div className="mx_RoomList_emptySubListTip">
{ _t(
"Press <StartChatButton> to start a chat with someone",
{},
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
) }
</div>;
break;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
tip = <div className="mx_RoomList_emptySubListTip">
{ _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
@ -585,6 +550,13 @@ module.exports = React.createClass({
},
) }
</div>;
break;
}
if (tip) {
return <div className="mx_RoomList_emptySubListTip_container">
{ tip }
</div>;
}
// We don't want to display drop targets if there are no room tiles to drag'n'drop
@ -627,13 +599,18 @@ module.exports = React.createClass({
return ret;
},
_collectGemini(gemScroll) {
this._gemScroll = gemScroll;
},
render: function() {
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
<div className="mx_RoomList">
<RoomSubList list={[]}
extraTiles={this._makeGroupInviteTiles()}
@ -652,7 +629,6 @@ module.exports = React.createClass({
editable={false}
order="recent"
isInvite={true}
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -666,7 +642,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('m.favourite')}
editable={true}
order="manual"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -680,7 +655,6 @@ module.exports = React.createClass({
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={true}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
@ -694,7 +668,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
headerItems={this._getHeaderItems('im.vector.fake.recent')}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -702,7 +675,7 @@ module.exports = React.createClass({
onShowMoreRooms={self.onShowMoreRooms} />
{ Object.keys(self.state.lists).map((tagName) => {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
if (!tagName.match(STANDARD_TAGS_REGEX)) {
return <RoomSubList list={self.state.lists[tagName]}
key={tagName}
label={tagName}
@ -710,7 +683,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent(tagName)}
editable={true}
order="manual"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -725,7 +697,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={true}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -736,7 +707,6 @@ module.exports = React.createClass({
label={_t('Historical')}
editable={false}
order="recent"
selectedRoom={self.props.selectedRoom}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
startAsHidden={true}
@ -746,7 +716,7 @@ module.exports = React.createClass({
searchFilter={self.props.searchFilter}
onShowMoreRooms={self.onShowMoreRooms} />
</div>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
);
},
});

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t } from '../../../languageHandler';
@ -25,7 +26,13 @@ module.exports = React.createClass({
displayName: 'RoomNameEditor',
propTypes: {
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
name: null,
};
},
componentWillMount: function() {
@ -34,7 +41,9 @@ module.exports = React.createClass({
const myId = MatrixClientPeg.get().credentials.userId;
const defaultName = room.getDefaultRoomName(myId);
this._initialName = name ? name.getContent().name : '';
this.setState({
name: name ? name.getContent().name : '',
});
this._placeholderName = _t("Unnamed Room");
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
@ -43,7 +52,13 @@ module.exports = React.createClass({
},
getRoomName: function() {
return this.refs.editor.getValue();
return this.state.name;
},
_onValueChanged: function(value, shouldSubmit) {
this.setState({
name: value,
});
},
render: function() {
@ -56,7 +71,8 @@ module.exports = React.createClass({
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={this._placeholderName}
blurToCancel={false}
initialValue={this._initialName}
initialValue={this.state.name}
onValueChanged={this._onValueChanged}
dir="auto" />
</div>
);

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -27,29 +28,29 @@ module.exports = React.createClass({
displayName: 'RoomPreviewBar',
propTypes: {
onJoinClick: React.PropTypes.func,
onRejectClick: React.PropTypes.func,
onForgetClick: React.PropTypes.func,
onJoinClick: PropTypes.func,
onRejectClick: PropTypes.func,
onForgetClick: PropTypes.func,
// if inviterName is specified, the preview bar will shown an invite to the room.
// You should also specify onRejectClick if specifiying inviterName
inviterName: React.PropTypes.string,
inviterName: PropTypes.string,
// If invited by 3rd party invite, the email address the invite was sent to
invitedEmail: React.PropTypes.string,
invitedEmail: PropTypes.string,
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error: React.PropTypes.object,
error: PropTypes.object,
canPreview: React.PropTypes.bool,
spinner: React.PropTypes.bool,
room: React.PropTypes.object,
canPreview: PropTypes.bool,
spinner: PropTypes.bool,
room: PropTypes.object,
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.string,
roomAlias: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
@ -60,10 +61,10 @@ const plEventsToShow = {
const BannedUser = React.createClass({
propTypes: {
canUnban: React.PropTypes.bool,
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
by: React.PropTypes.string.isRequired,
reason: React.PropTypes.string,
canUnban: PropTypes.bool,
member: PropTypes.object.isRequired, // js-sdk RoomMember
by: PropTypes.string.isRequired,
reason: PropTypes.string,
},
_onUnbanClick: function() {
@ -115,8 +116,7 @@ module.exports = React.createClass({
displayName: 'RoomSettings',
propTypes: {
room: React.PropTypes.object.isRequired,
onSaveClick: React.PropTypes.func,
room: PropTypes.object.isRequired,
},
getInitialState: function() {
@ -131,7 +131,8 @@ module.exports = React.createClass({
join_rule: this._yankValueFromEvent("m.room.join_rules", "join_rule"),
history_visibility: this._yankValueFromEvent("m.room.history_visibility", "history_visibility"),
guest_access: this._yankValueFromEvent("m.room.guest_access", "guest_access"),
power_levels_changed: false,
powerLevels: this._yankContentFromEvent("m.room.power_levels", {}),
powerLevelsChanged: false,
tags_changed: false,
tags: tags,
// isRoomPublished is loaded async in componentWillMount so when the component
@ -150,7 +151,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().getRoomDirectoryVisibility(
this.props.room.roomId,
).done((result) => {
).done((result = {}) => {
this.setState({ isRoomPublished: result.visibility === "public" });
this._originalIsRoomPublished = result.visibility === "public";
}, (err) => {
@ -271,8 +272,8 @@ module.exports = React.createClass({
// power levels
const powerLevels = this._getPowerLevels();
if (powerLevels) {
const powerLevels = this.state.powerLevels;
if (this.state.powerLevelsChanged) {
promises.push(MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.power_levels", powerLevels, "",
));
@ -383,36 +384,32 @@ module.exports = React.createClass({
return strA !== strB;
},
_getPowerLevels: function() {
if (!this.state.power_levels_changed) return undefined;
onPowerLevelsChanged: function(value, powerLevelKey) {
const powerLevels = Object.assign({}, this.state.powerLevels);
const eventsLevelPrefix = "event_levels_";
let powerLevels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
powerLevels = powerLevels ? powerLevels.getContent() : {};
value = parseInt(value);
for (const key of Object.keys(this.refs).filter((k) => k.startsWith("event_levels_"))) {
const eventType = key.substring("event_levels_".length);
powerLevels.events[eventType] = parseInt(this.refs[key].getValue());
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy
powerLevels["events"] = Object.assign({}, this.state.powerLevels["events"] || {});
powerLevels["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else {
powerLevels[powerLevelKey] = value;
}
const newPowerLevels = {
ban: parseInt(this.refs.ban.getValue()),
kick: parseInt(this.refs.kick.getValue()),
redact: parseInt(this.refs.redact.getValue()),
invite: parseInt(this.refs.invite.getValue()),
events_default: parseInt(this.refs.events_default.getValue()),
state_default: parseInt(this.refs.state_default.getValue()),
users_default: parseInt(this.refs.users_default.getValue()),
users: powerLevels.users,
events: powerLevels.events,
};
return newPowerLevels;
this.setState({
powerLevels,
powerLevelsChanged: true,
});
},
onPowerLevelsChanged: function() {
this.setState({
power_levels_changed: true,
});
_yankContentFromEvent: function(stateEventType, defaultValue) {
// E.g.("m.room.name") would yank the content of "m.room.name"
const event = this.props.room.currentState.getStateEvents(stateEventType, '');
if (!event) {
return defaultValue;
}
return event.getContent() || defaultValue;
},
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
@ -632,29 +629,61 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const user_id = cli.credentials.userId;
const myUserId = cli.credentials.userId;
const power_level_event = roomState.getStateEvents('m.room.power_levels', '');
const power_levels = power_level_event ? power_level_event.getContent() : {};
const events_levels = power_levels.events || {};
const user_levels = power_levels.users || {};
const powerLevels = this.state.powerLevels;
const eventsLevels = powerLevels.events || {};
const userLevels = powerLevels.users || {};
const ban_level = parseIntWithDefault(power_levels.ban, 50);
const kick_level = parseIntWithDefault(power_levels.kick, 50);
const redact_level = parseIntWithDefault(power_levels.redact, 50);
const invite_level = parseIntWithDefault(power_levels.invite, 50);
const send_level = parseIntWithDefault(power_levels.events_default, 0);
const state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
const default_user_level = parseIntWithDefault(power_levels.users_default, 0);
const powerLevelDescriptors = {
users_default: {
desc: _t('The default role for new room members is'),
defaultValue: 0,
},
events_default: {
desc: _t('To send messages, you must be a'),
defaultValue: 0,
},
invite: {
desc: _t('To invite users into the room, you must be a'),
defaultValue: 50,
},
state_default: {
desc: _t('To configure the room, you must be a'),
defaultValue: 50,
},
kick: {
desc: _t('To kick users, you must be a'),
defaultValue: 50,
},
ban: {
desc: _t('To ban users, you must be a'),
defaultValue: 50,
},
redact: {
desc: _t('To remove other users\' messages, you must be a'),
defaultValue: 50,
},
};
this._populateDefaultPlEvents(events_levels, state_level, send_level);
const banLevel = parseIntWithDefault(powerLevels.ban, powerLevelDescriptors.ban.defaultValue);
const defaultUserLevel = parseIntWithDefault(
powerLevels.users_default,
powerLevelDescriptors.users_default.defaultValue,
);
let current_user_level = user_levels[user_id];
if (current_user_level === undefined) {
current_user_level = default_user_level;
this._populateDefaultPlEvents(
eventsLevels,
parseIntWithDefault(powerLevels.state_default, powerLevelDescriptors.state_default.defaultValue),
parseIntWithDefault(powerLevels.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let currentUserLevel = userLevels[myUserId];
if (currentUserLevel === undefined) {
currentUserLevel = defaultUserLevel;
}
const can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
const canChangeLevels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
const canSetTag = !cli.isGuest();
@ -667,15 +696,18 @@ module.exports = React.createClass({
/>;
let userLevelsSection;
if (Object.keys(user_levels).length) {
if (Object.keys(userLevels).length) {
userLevelsSection =
<div>
<h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{ Object.keys(user_levels).map(function(user, i) {
{ Object.keys(userLevels).map(function(user, i) {
return (
<li className="mx_RoomSettings_userLevel" key={user}>
{ _t("%(user)s is a", {user: user}) } <PowerSelector value={user_levels[user]} disabled={true} />
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</li>
);
}) }
@ -688,7 +720,7 @@ module.exports = React.createClass({
const banned = this.props.room.getMembersWithMembership("ban");
let bannedUsersSection;
if (banned.length) {
const canBanUsers = current_user_level >= ban_level;
const canBanUsers = currentUserLevel >= banLevel;
bannedUsersSection =
<div>
<h3>{ _t('Banned users') }</h3>
@ -710,13 +742,13 @@ module.exports = React.createClass({
if (this._yankValueFromEvent("m.room.create", "m.federate", true) === false) {
unfederatableSection = (
<div className="mx_RoomSettings_powerLevel">
{ _t('This room is not accessible by remote Matrix servers') }.
{ _t('This room is not accessible by remote Matrix servers') }.
</div>
);
}
let leaveButton = null;
const myMember = this.props.room.getMember(user_id);
const myMember = this.props.room.getMember(myUserId);
if (myMember) {
if (myMember.membership === "join") {
leaveButton = (
@ -773,7 +805,8 @@ module.exports = React.createClass({
const aliasEvents = this.props.room.currentState.getStateEvents('m.room.aliases') || [];
let aliasCount = 0;
aliasEvents.forEach((event) => {
aliasCount += event.getContent().aliases.length;
const aliases = event.getContent().aliases || [];
aliasCount += aliases.length;
});
if (this.state.join_rule === "public" && aliasCount == 0) {
@ -798,6 +831,50 @@ module.exports = React.createClass({
</div>;
}
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
const descriptor = powerLevelDescriptors[key];
const value = parseIntWithDefault(powerLevels[key], descriptor.defaultValue);
return <div key={index} className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">
{ descriptor.desc }
</span>
<PowerSelector
value={value}
usersDefault={defaultUserLevel}
controlled={false}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this.onPowerLevelsChanged}
/>
</div>;
});
const eventPowerSelectors = Object.keys(eventsLevels).map(function(eventType, i) {
let label = plEventsToLabels[eventType];
if (label) {
label = _t(label);
} else {
label = _t(
"To send events of type <eventType/>, you must be a", {},
{ 'eventType': <code>{ eventType }</code> },
);
}
return (
<div className="mx_RoomSettings_powerLevel" key={eventType}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
<PowerSelector
value={eventsLevels[eventType]}
usersDefault={defaultUserLevel}
controlled={false}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType}
onChange={self.onPowerLevelsChanged}
/>
</div>
);
});
return (
<div className="mx_RoomSettings">
@ -897,49 +974,9 @@ module.exports = React.createClass({
<h3>{ _t('Permissions') }</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
</div>
{ Object.keys(events_levels).map(function(event_type, i) {
let label = plEventsToLabels[event_type];
if (label) label = _t(label);
else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
return (
<div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} usersDefault={default_user_level} onChange={self.onPowerLevelsChanged}
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
</div>
);
}) }
{ unfederatableSection }
{ powerSelectors }
{ eventPowerSelectors }
{ unfederatableSection }
</div>
{ userLevelsSection }

View file

@ -19,7 +19,9 @@ limitations under the License.
const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const classNames = require('classnames');
import dis from '../../../dispatcher';
const MatrixClientPeg = require('../../../MatrixClientPeg');
import DMRoomMap from '../../../utils/DMRoomMap';
const sdk = require('../../../index');
@ -34,17 +36,16 @@ module.exports = React.createClass({
displayName: 'RoomTile',
propTypes: {
connectDragSource: React.PropTypes.func,
connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool,
onClick: PropTypes.func,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object,
room: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired,
unread: PropTypes.bool.isRequired,
highlight: PropTypes.bool.isRequired,
// If true, apply mx_RoomTile_transparent class
transparent: PropTypes.bool,
isInvite: PropTypes.bool.isRequired,
incomingCall: PropTypes.object,
},
getDefaultProps: function() {
@ -58,7 +59,9 @@ module.exports = React.createClass({
hover: false,
badgeHover: false,
menuDisplayed: false,
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
@ -81,6 +84,20 @@ module.exports = React.createClass({
}
},
onRoomTimeline: function(ev, room) {
if (room !== this.props.room) return;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
onRoomName: function(room) {
if (room !== this.props.room) return;
this.setState({
roomName: this.props.room.name,
});
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
@ -89,6 +106,21 @@ module.exports = React.createClass({
}
},
onAction: function(payload) {
switch (payload.action) {
// XXX: slight hack in order to zero the notification count when a room
// is read. Ideally this state would be given to this via props (as we
// do with `unread`). This is still better than forceUpdating the entire
// RoomList when a room is read.
case 'on_room_read':
if (payload.roomId !== this.props.room.roomId) break;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
break;
}
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
@ -97,15 +129,46 @@ module.exports = React.createClass({
componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef);
},
componentWillReceiveProps: function(props) {
// XXX: This could be a lot better - this makes the assumption that
// the notification count may have changed when the properties of
// the room tile change.
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
// Do a simple shallow comparison of props and state to avoid unnecessary
// renders. The assumption made here is that only state and props are used
// in rendering this component and children.
//
// RoomList is frequently made to forceUpdate, so this decreases number of
// RoomTile renderings.
shouldComponentUpdate: function(newProps, newState) {
if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
return true;
}
if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
return true;
}
return false;
},
onClick: function(ev) {
@ -174,7 +237,7 @@ module.exports = React.createClass({
const myUserId = MatrixClientPeg.get().credentials.userId;
const me = this.props.room.currentState.members[myUserId];
const notificationCount = this.props.room.getUnreadNotificationCount();
const notificationCount = this.state.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
@ -190,6 +253,7 @@ module.exports = React.createClass({
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
});
const avatarClasses = classNames({
@ -201,9 +265,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
});
// XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k)
let name = this.props.room.name || this.props.room.roomId;
let name = this.state.roomName;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge;
@ -255,35 +317,19 @@ module.exports = React.createClass({
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
}
// These props are injected by React DnD,
// as defined by your `collect` function above:
const isDragging = this.props.isDragging;
const connectDragSource = this.props.connectDragSource;
const connectDropTarget = this.props.connectDropTarget;
let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */ }
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ directMessageIndicator }
</div>
return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ directMessageIndicator }
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
</div>
);
if (connectDropTarget) ret = connectDropTarget(ret);
if (connectDragSource) ret = connectDragSource(ret);
return ret;
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>;
},
});

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
import { _t } from "../../../languageHandler";
@ -24,29 +25,44 @@ module.exports = React.createClass({
displayName: 'RoomTopicEditor',
propTypes: {
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
topic: null,
};
},
componentWillMount: function() {
const room = this.props.room;
const topic = room.currentState.getStateEvents('m.room.topic', '');
this._initialTopic = topic ? topic.getContent().topic : '';
this.setState({
topic: topic ? topic.getContent().topic : '',
});
},
getTopic: function() {
return this.refs.editor.getValue();
return this.state.topic;
},
_onValueChanged: function(value) {
this.setState({
topic: value,
});
},
render: function() {
const EditableText = sdk.getComponent("elements.EditableText");
return (
<EditableText ref="editor"
<EditableText
className="mx_RoomHeader_topic mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={_t("Add a topic")}
blurToCancel={false}
initialValue={this._initialTopic}
initialValue={this.state.topic}
onValueChanged={this._onValueChanged}
dir="auto" />
);
},

Some files were not shown because too many files have changed in this diff Show more