Merge branch 'develop' into t3chguy/hide_left_chats_memberinfo

This commit is contained in:
Michael Telatynski 2018-01-04 16:41:16 +00:00 committed by GitHub
commit 831a3f7e42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
311 changed files with 28762 additions and 12120 deletions

View file

@ -27,6 +27,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils';
import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -81,16 +82,25 @@ module.exports = React.createClass({
},
onAction: function(action) {
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
switch (action.action) {
case 'appsDrawer':
// When opening the app draw when there aren't any apps, auto-launch the
// integrations manager to skip the awkward click on "Add widget"
// When opening the app drawer when there aren't any apps,
// auto-launch the integrations manager to skip the awkward
// click on "Add widget"
if (action.show) {
const apps = this._getApps();
if (apps.length === 0) {
this._launchManageIntegrations();
}
localStorage.removeItem(hideWidgetKey);
} else {
// Store hidden state of widget
// Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true);
}
break;
}
},
@ -122,16 +132,22 @@ module.exports = React.createClass({
'$matrix_room_id': this.props.room.roomId,
'$matrix_display_name': user ? user.displayName : this.props.userId,
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
};
if(app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
});
}
// TODO: Namespace themes through some standard
'$theme': SettingsStore.getValue("theme"),
};
app.id = appId;
app.name = app.name || app.type;
if (app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
});
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
}
app.url = this.encodeUri(app.url, params);
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
@ -168,7 +184,7 @@ module.exports = React.createClass({
_canUserModify: function() {
try {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
} catch(err) {
} catch (err) {
console.error(err);
return false;
}
@ -215,6 +231,8 @@ module.exports = React.createClass({
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
waitForIframeLoad={app.waitForIframeLoad}
/>);
});
@ -231,16 +249,16 @@ module.exports = React.createClass({
"mx_AddWidget_button"
}
title={_t('Add a widget')}>
[+] {_t('Add a widget')}
[+] { _t('Add a widget') }
</div>;
}
return (
<div className="mx_AppsDrawer">
<div id="apps" className="mx_AppsContainer">
{apps}
{ apps }
</div>
{this._canUserModify() && addWidget}
{ this._canUserModify() && addWidget }
</div>
);
},

View file

@ -1,14 +1,34 @@
/*
Copyright 2016 Aviral Dasgupta
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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { Room } from 'matrix-js-sdk';
import {getCompletions} from '../../../autocomplete/Autocompleter';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
@ -17,6 +37,7 @@ export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.autocompleter = new Autocompleter(props.room);
this.completionPromise = null;
this.hide = this.hide.bind(this);
this.onCompletionClicked = this.onCompletionClicked.bind(this);
@ -41,6 +62,11 @@ export default class Autocomplete extends React.Component {
}
componentWillReceiveProps(newProps, state) {
if (this.props.room.roomId !== newProps.room.roomId) {
this.autocompleter.destroy();
this.autocompleter = new Autocompleter(newProps.room);
}
// Query hasn't changed so don't try to complete it
if (newProps.query === this.props.query) {
return;
@ -49,6 +75,10 @@ export default class Autocomplete extends React.Component {
this.complete(newProps.query, newProps.selection);
}
componentWillUnmount() {
this.autocompleter.destroy();
}
complete(query, selection) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
@ -66,7 +96,7 @@ export default class Autocomplete extends React.Component {
});
return Promise.resolve(null);
}
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) {
@ -83,7 +113,7 @@ export default class Autocomplete extends React.Component {
}
processQuery(query, selection) {
return getCompletions(
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
@ -233,7 +263,7 @@ export default class Autocomplete extends React.Component {
const componentPosition = position;
position++;
const onMouseOver = () => this.setSelection(componentPosition);
const onMouseMove = () => this.setSelection(componentPosition);
const onClick = () => {
this.setSelection(componentPosition);
this.onCompletionClicked();
@ -243,7 +273,7 @@ export default class Autocomplete extends React.Component {
key: i,
ref: `completion${position - 1}`,
className,
onMouseOver,
onMouseMove,
onClick,
});
});
@ -251,15 +281,15 @@ export default class Autocomplete extends React.Component {
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
{completionResult.provider.renderCompletions(completions)}
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
{ completionResult.provider.renderCompletions(completions) }
</div>
) : null;
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
{renderedCompletions}
{ renderedCompletions }
</div>
) : null;
}
@ -267,8 +297,11 @@ export default class Autocomplete extends React.Component {
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: React.PropTypes.string.isRequired,
query: PropTypes.string.isRequired,
// method invoked with range and text content when completion is confirmed
onConfirm: React.PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
// The room in which we're autocompleting
room: PropTypes.instanceOf(Room),
};

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.
@ -20,8 +21,7 @@ import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -83,9 +83,9 @@ module.exports = React.createClass({
<div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel"
title={_t("Drop File Here")}>
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
<br/>
{_t("Drop file here to upload")}
<TintableSvg src="img/upload-big.svg" width="45" height="59" />
<br />
{ _t("Drop file here to upload") }
</div>
</div>
);
@ -99,23 +99,23 @@ module.exports = React.createClass({
supportedText = _t(" (unsupported)");
} else {
joinNode = (<span>
{_tJsx(
{ _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
[
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}</a>,
]
)}
{},
{
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
},
) }
</span>);
}
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
// but there are translations for this in the languages we do have so I'm leaving it for now.
conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification">
{_t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText})}
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
&nbsp;
{joinNode}
{ joinNode }
</div>
);
}
@ -128,15 +128,12 @@ module.exports = React.createClass({
/>
);
let appsDrawer = null;
if(UserSettingsStore.isFeatureEnabled('matrix_apps')) {
appsDrawer = <AppsDrawer ref="appsDrawer"
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
/>;
}
const appsDrawer = <AppsDrawer ref="appsDrawer"
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
/>;
return (
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
var PRESENCE_CLASS = {
const PRESENCE_CLASS = {
"offline": "mx_EntityTile_offline",
"online": "mx_EntityTile_online",
"unavailable": "mx_EntityTile_unavailable"
"unavailable": "mx_EntityTile_unavailable",
};
@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) {
}
}
module.exports = React.createClass({
const EntityTile = React.createClass({
displayName: 'EntityTile',
propTypes: {
@ -62,7 +62,7 @@ module.exports = React.createClass({
showInviteButton: React.PropTypes.bool,
shouldComponentUpdate: React.PropTypes.func,
onClick: React.PropTypes.func,
suppressOnHover: React.PropTypes.bool
suppressOnHover: React.PropTypes.bool,
},
getDefaultProps: function() {
@ -73,13 +73,13 @@ module.exports = React.createClass({
presenceLastActiveAgo: 0,
presenceLastTs: 0,
showInviteButton: false,
suppressOnHover: false
suppressOnHover: false,
};
},
getInitialState: function() {
return {
hover: false
hover: false,
};
},
@ -98,38 +98,39 @@ module.exports = React.createClass({
render: function() {
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo
this.props.presenceState, this.props.presenceLastActiveAgo,
);
var mainClassName = "mx_EntityTile ";
let mainClassName = "mx_EntityTile ";
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
var nameEl;
let nameEl;
const {name} = this.props;
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this.state.hover && !this.props.suppressOnHover) {
var activeAgo = this.props.presenceLastActiveAgo ?
const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
mainClassName += " mx_EntityTile_hover";
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
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_hover" dir="auto">{name}</EmojiText>
<PresenceLabel activeAgo={ activeAgo }
<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">
{ name }
</EmojiText>
<PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
</div>
);
}
else {
} else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{name}</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
);
}
var inviteButton;
let inviteButton;
if (this.props.showInviteButton) {
inviteButton = (
<div className="mx_EntityTile_invite">
@ -138,25 +139,28 @@ module.exports = React.createClass({
);
}
var power;
var powerLevel = this.props.powerLevel;
if (powerLevel >= 50 && powerLevel < 99) {
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")}/>;
}
if (powerLevel >= 99) {
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")}/>;
let power;
const powerStatus = this.props.powerStatus;
if (powerStatus) {
const src = {
[EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg",
[EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg",
}[powerStatus];
const alt = {
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
return (
<AccessibleButton className={mainClassName} title={ this.props.title }
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
onMouseLeave={ this.mouseLeave }>
<AccessibleButton className={mainClassName} title={this.props.title}
onClick={this.props.onClick} onMouseEnter={this.mouseEnter}
onMouseLeave={this.mouseLeave}>
<div className="mx_EntityTile_avatar">
{ av }
{ power }
@ -165,5 +169,11 @@ module.exports = React.createClass({
{ inviteButton }
</AccessibleButton>
);
}
},
});
EntityTile.POWER_STATUS_MODERATOR = "moderator";
EntityTile.POWER_STATUS_ADMIN = "admin";
export default EntityTile;

View file

@ -17,38 +17,47 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require("classnames");
import { _t } from '../../../languageHandler';
var Modal = require('../../../Modal');
const React = require('react');
const classNames = require("classnames");
import { _t, _td } from '../../../languageHandler';
const Modal = require('../../../Modal');
var sdk = require('../../../index');
var TextForEvent = require('../../../TextForEvent');
const sdk = require('../../../index');
const TextForEvent = require('../../../TextForEvent');
import withMatrixClient from '../../../wrappers/withMatrixClient';
var ContextualMenu = require('../../structures/ContextualMenu');
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
var ObjectUtils = require('../../../ObjectUtils');
const ObjectUtils = require('../../../ObjectUtils');
var eventTileTypes = {
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.room.member' : 'messages.TextualEvent',
'm.call.invite' : 'messages.TextualEvent',
'm.call.answer' : 'messages.TextualEvent',
'm.call.hangup' : 'messages.TextualEvent',
'm.room.name' : 'messages.TextualEvent',
'm.room.avatar' : 'messages.RoomAvatarEvent',
'm.room.topic' : 'messages.TextualEvent',
'm.room.third_party_invite' : 'messages.TextualEvent',
'm.room.history_visibility' : 'messages.TextualEvent',
'm.room.encryption' : 'messages.TextualEvent',
'm.room.power_levels' : 'messages.TextualEvent',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
};
const stateEventTileTypes = {
'm.room.member': 'messages.TextualEvent',
'm.room.name': 'messages.TextualEvent',
'm.room.avatar': 'messages.RoomAvatarEvent',
'm.room.third_party_invite': 'messages.TextualEvent',
'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};
var MAX_READ_AVATARS = 5;
function getHandlerTile(ev) {
const type = ev.getType();
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is:
//
@ -177,7 +186,7 @@ module.exports = withMatrixClient(React.createClass({
},
componentWillUnmount: function() {
var client = this.props.matrixClient;
const client = this.props.matrixClient;
client.removeListener("deviceVerificationChanged",
this.onDeviceVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
@ -187,6 +196,8 @@ module.exports = withMatrixClient(React.createClass({
*/
_onDecrypted: function() {
// we need to re-verify the sending device.
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption
// has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent);
this.forceUpdate();
},
@ -204,20 +215,23 @@ module.exports = withMatrixClient(React.createClass({
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
this.setState({
verified: verified
verified: verified,
}, () => {
// Decryption may have caused a change in size
this.props.onWidgetLoad();
});
},
_propsEqual: function(objA, objB) {
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (!objB.hasOwnProperty(key)) {
return false;
@ -225,8 +239,8 @@ module.exports = withMatrixClient(React.createClass({
// need to deep-compare readReceipts
if (key == 'readReceipts') {
var rA = objA[key];
var rB = objB[key];
const rA = objA[key];
const rB = objB[key];
if (rA === rB) {
continue;
}
@ -238,7 +252,7 @@ module.exports = withMatrixClient(React.createClass({
if (rA.length !== rB.length) {
return false;
}
for (var j = 0; j < rA.length; j++) {
for (let j = 0; j < rA.length; j++) {
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
return false;
}
@ -253,12 +267,11 @@ module.exports = withMatrixClient(React.createClass({
},
shouldHighlight: function() {
var actions = this.props.matrixClient.getPushActionsForEvent(this.props.mxEvent);
const actions = this.props.matrixClient.getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients
if (this.props.mxEvent.getSender() === this.props.matrixClient.credentials.userId)
{
if (this.props.mxEvent.getSender() === this.props.matrixClient.credentials.userId) {
return false;
}
@ -266,13 +279,13 @@ module.exports = withMatrixClient(React.createClass({
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect();
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
var x = buttonRect.right + window.pageXOffset;
var y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
var self = this;
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const self = this;
ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
@ -281,19 +294,18 @@ module.exports = withMatrixClient(React.createClass({
eventTileOps: this.refs.tile && this.refs.tile.getEventTileOps ? this.refs.tile.getEventTileOps() : undefined,
onFinished: function() {
self.setState({menu: false});
}
},
});
this.setState({menu: true});
},
toggleAllReadAvatars: function() {
this.setState({
allReadAvatars: !this.state.allReadAvatars
allReadAvatars: !this.state.allReadAvatars,
});
},
getReadAvatars: function() {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars"></span>);
@ -304,11 +316,11 @@ module.exports = withMatrixClient(React.createClass({
const receiptOffset = 15;
let left = 0;
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) {
var receipt = receipts[i];
const receipts = this.props.readReceipts || [];
for (let i = 0; i < receipts.length; ++i) {
const receipt = receipts[i];
var hidden = true;
let hidden = true;
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
hidden = false;
}
@ -319,7 +331,7 @@ module.exports = withMatrixClient(React.createClass({
// else set it proportional to index
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
var userId = receipt.roomMember.userId;
const userId = receipt.roomMember.userId;
var readReceiptInfo;
if (this.props.readReceiptMap) {
@ -340,12 +352,12 @@ module.exports = withMatrixClient(React.createClass({
onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts}
showTwelveHour={this.props.isTwelveHour}
/>
/>,
);
}
var remText;
let remText;
if (!this.state.allReadAvatars) {
var remainder = receipts.length - MAX_READ_AVATARS;
const remainder = receipts.length - MAX_READ_AVATARS;
if (remainder > 0) {
remText = <span className="mx_EventTile_readAvatarRemainder"
onClick={this.toggleAllReadAvatars}
@ -369,7 +381,7 @@ module.exports = withMatrixClient(React.createClass({
},
onCryptoClicked: function(e) {
var event = this.props.mxEvent;
const event = this.props.mxEvent;
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => {
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
@ -396,12 +408,12 @@ module.exports = withMatrixClient(React.createClass({
if (ev.getContent().msgtype === 'm.bad.encrypted') {
return <E2ePadlockUndecryptable {...props}/>;
return <E2ePadlockUndecryptable {...props} />;
} else if (ev.isEncrypted()) {
if (this.state.verified) {
return <E2ePadlockVerified {...props}/>;
return <E2ePadlockVerified {...props} />;
} else {
return <E2ePadlockUnverified {...props}/>;
return <E2ePadlockUnverified {...props} />;
}
} else {
// XXX: if the event is being encrypted (ie eventSendStatus ===
@ -412,7 +424,7 @@ module.exports = withMatrixClient(React.createClass({
// open padlock
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
if (e2eEnabled) {
return <E2ePadlockUnencrypted {...props}/>;
return <E2ePadlockUnencrypted {...props} />;
}
}
@ -421,27 +433,27 @@ module.exports = withMatrixClient(React.createClass({
},
render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
const SenderProfile = sdk.getComponent('messages.SenderProfile');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
var eventType = this.props.mxEvent.getType();
const content = this.props.mxEvent.getContent();
const msgtype = content.msgtype;
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
var isInfoMessage = (eventType !== 'm.room.message');
const isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
throw new Error("Event type not supported");
}
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
const classes = classNames({
@ -468,9 +480,9 @@ module.exports = withMatrixClient(React.createClass({
this.props.mxEvent.getRoomId() + "/" +
this.props.mxEvent.getId();
var readAvatars = this.getReadAvatars();
const readAvatars = this.getReadAvatars();
var avatar, sender;
let avatar, sender;
let avatarSize;
let needsSenderProfile;
@ -503,20 +515,19 @@ module.exports = withMatrixClient(React.createClass({
}
if (needsSenderProfile) {
let aux = null;
let text = null;
if (!this.props.tileShape) {
if (msgtype === 'm.image') aux = _t('sent an image');
else if (msgtype === 'm.video') aux = _t('sent a video');
else if (msgtype === 'm.file') aux = _t('uploaded a file');
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
else {
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
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');
sender = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} enableFlair={!text} text={text} />;
} else {
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
}
}
const editButton = (
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
<span className="mx_EventTile_editButton" title={_t("Options")} onClick={this.onEditClicked} />
);
const timestamp = this.props.mxEvent.getTs() ?
@ -527,13 +538,13 @@ module.exports = withMatrixClient(React.createClass({
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={ permalink } onClick={this.onPermalinkClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={ permalink } onClick={this.onPermalinkClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
@ -548,8 +559,7 @@ module.exports = withMatrixClient(React.createClass({
</div>
</div>
);
}
else if (this.props.tileShape === "file_grid") {
} else if (this.props.tileShape === "file_grid") {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
@ -563,7 +573,7 @@ module.exports = withMatrixClient(React.createClass({
</div>
<a
className="mx_EventTile_senderDetailsLink"
href={ permalink }
href={permalink}
onClick={this.onPermalinkClicked}
>
<div className="mx_EventTile_senderDetails">
@ -573,8 +583,7 @@ module.exports = withMatrixClient(React.createClass({
</a>
</div>
);
}
else {
} else {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
@ -583,7 +592,7 @@ module.exports = withMatrixClient(React.createClass({
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={ permalink } onClick={this.onPermalinkClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
@ -604,8 +613,10 @@ module.exports = withMatrixClient(React.createClass({
module.exports.haveTileForEvent = function(e) {
// Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';
} else {
return true;

View file

@ -18,7 +18,7 @@
import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
import { KeyCode } from '../../../Keyboard';
module.exports = React.createClass({
@ -30,10 +30,9 @@ module.exports = React.createClass({
componentWillMount: function() {
dis.dispatch({
action: 'ui_opacity',
leftOpacity: 1.0,
rightOpacity: 0.3,
middleOpacity: 0.5,
action: 'panel_disable',
rightDisabled: true,
middleDisabled: true,
});
},
@ -43,9 +42,9 @@ module.exports = React.createClass({
componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
action: 'panel_disable',
sideDisabled: false,
middleDisabled: false,
});
document.removeEventListener('keydown', this._onKeyDown);
},
@ -61,7 +60,7 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_ForwardMessage">
<h1>{_t('Please select the destination room for this message')}</h1>
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
},

View file

@ -16,16 +16,16 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var ImageUtils = require('../../../ImageUtils');
var Modal = require('../../../Modal');
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const ImageUtils = require('../../../ImageUtils');
const Modal = require('../../../Modal');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
const linkify = require('linkifyjs');
const linkifyElement = require('linkifyjs/element');
const linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({
@ -40,7 +40,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
preview: null
preview: null,
};
},
@ -52,7 +52,7 @@ module.exports = React.createClass({
}
this.setState(
{ preview: res },
this.props.onWidgetLoad
this.props.onWidgetLoad,
);
}, (error)=>{
console.error("Failed to get preview for " + this.props.link + " " + error);
@ -76,17 +76,17 @@ module.exports = React.createClass({
},
onImageClick: function(ev) {
var p = this.state.preview;
const p = this.state.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
var ImageView = sdk.getComponent("elements.ImageView");
const ImageView = sdk.getComponent("elements.ImageView");
var src = p["og:image"];
let src = p["og:image"];
if (src && src.startsWith("mxc://")) {
src = MatrixClientPeg.get().mxcUrlToHttp(src);
}
var params = {
const params = {
src: src,
width: p["og:image:width"],
height: p["og:image:height"],
@ -99,27 +99,27 @@ module.exports = React.createClass({
},
render: function() {
var p = this.state.preview;
const p = this.state.preview;
if (!p || Object.keys(p).length === 0) {
return <div/>;
return <div />;
}
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
var image = p["og:image"];
var imageMaxWidth = 100, imageMaxHeight = 100;
let image = p["og:image"];
let imageMaxWidth = 100, imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
}
var thumbHeight = imageMaxHeight;
let thumbHeight = imageMaxHeight;
if (p["og:image:width"] && p["og:image:height"]) {
thumbHeight = ImageUtils.thumbHeight(p["og:image:width"], p["og:image:height"], imageMaxWidth, imageMaxHeight);
}
var img;
let img;
if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
</div>;
}
@ -127,15 +127,16 @@ module.exports = React.createClass({
<div className="mx_LinkPreviewWidget" >
{ img }
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank" rel="noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref="description">
{ p["og:description"] }
</div>
</div>
<img className="mx_LinkPreviewWidget_cancel" src="img/cancel.svg" width="18" height="18"
onClick={ this.props.onCancelClick }/>
<img className="mx_LinkPreviewWidget_cancel mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18"
onClick={this.props.onCancelClick} />
</div>
);
}
},
});

View file

@ -20,30 +20,30 @@ import { _t } from '../../../languageHandler';
export default class MemberDeviceInfo extends React.Component {
render() {
var indicator = null;
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
let indicator = null;
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
if (this.props.device.isBlocked()) {
indicator = (
<div className="mx_MemberDeviceInfo_blacklisted">
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")}/>
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")} />
</div>
);
} else if (this.props.device.isVerified()) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">
<img src="img/e2e-verified.svg" width="10" height="12" alt={_t("Verified")}/>
<img src="img/e2e-verified.svg" width="10" height="12" alt={_t("Verified")} />
</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")}/>
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")} />
</div>
);
}
var deviceName = this.props.device.ambiguous ?
const deviceName = this.props.device.ambiguous ?
(this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" :
this.props.device.getDisplayName();
@ -53,8 +53,8 @@ export default class MemberDeviceInfo extends React.Component {
title={_t("device id: ") + this.props.device.deviceId} >
<div className="mx_MemberDeviceInfo_deviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">
{deviceName}
{indicator}
{ deviceName }
{ indicator }
</div>
</div>
<DeviceVerifyButtons userId={this.props.userId} device={this.props.device} />

View file

@ -39,6 +39,7 @@ 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';
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
@ -54,13 +55,14 @@ module.exports = withMatrixClient(React.createClass({
kick: false,
ban: false,
mute: false,
modifyLevel: false
modifyLevel: false,
},
muted: false,
isTargetMod: false,
updating: 0,
devicesLoading: true,
devices: null,
isIgnoring: false,
};
},
@ -79,7 +81,10 @@ module.exports = withMatrixClient(React.createClass({
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomMember.membership", this.onRoomMemberMembership);
cli.on("accountData", this.onAccountData);
this._checkIgnoreState();
},
componentDidMount: function() {
@ -87,13 +92,13 @@ module.exports = withMatrixClient(React.createClass({
},
componentWillReceiveProps: function(newProps) {
if (this.props.member.userId != newProps.member.userId) {
if (this.props.member.userId !== newProps.member.userId) {
this._updateStateForNewMember(newProps.member);
}
},
componentWillUnmount: function() {
var client = this.props.matrixClient;
const client = this.props.matrixClient;
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("Room", this.onRoom);
@ -103,6 +108,7 @@ module.exports = withMatrixClient(React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("RoomState.events", this.onRoomStateEvents);
client.removeListener("RoomMember.name", this.onRoomMemberName);
client.removeListener("RoomMember.membership", this.onRoomMemberMembership);
client.removeListener("accountData", this.onAccountData);
}
if (this._cancelDeviceList) {
@ -110,15 +116,20 @@ module.exports = withMatrixClient(React.createClass({
}
},
_checkIgnoreState: function() {
const isIgnoring = this.props.matrixClient.isUserIgnored(this.props.member.userId);
this.setState({isIgnoring: isIgnoring});
},
_disambiguateDevices: function(devices) {
var names = Object.create(null);
for (var i = 0; i < devices.length; i++) {
var name = devices[i].getDisplayName();
var indexList = names[name] || [];
const names = Object.create(null);
for (let i = 0; i < devices.length; i++) {
const name = devices[i].getDisplayName();
const indexList = names[name] || [];
indexList.push(i);
names[name] = indexList;
}
for (name in names) {
for (const name in names) {
if (names[name].length > 1) {
names[name].forEach((j)=>{
devices[j].ambiguous = true;
@ -132,7 +143,7 @@ module.exports = withMatrixClient(React.createClass({
return;
}
if (userId == this.props.member.userId) {
if (userId === this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
@ -177,14 +188,18 @@ module.exports = withMatrixClient(React.createClass({
this.forceUpdate();
},
onRoomMemberMembership: function(ev, member) {
if (this.props.member.userId === member.userId) this.forceUpdate();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
if (ev.getType() === 'm.direct') {
this.forceUpdate();
}
},
_updateStateForNewMember: function(member) {
var newState = this._calculateOpsPermissions(member);
const newState = this._calculateOpsPermissions(member);
newState.devicesLoading = true;
newState.devices = null;
this.setState(newState);
@ -202,11 +217,11 @@ module.exports = withMatrixClient(React.createClass({
return;
}
var cancelled = false;
let cancelled = false;
this._cancelDeviceList = function() { cancelled = true; };
var client = this.props.matrixClient;
var self = this;
const client = this.props.matrixClient;
const self = this;
client.downloadKeys([member.userId], true).then(() => {
return client.getStoredDevicesForUser(member.userId);
}).finally(function() {
@ -224,14 +239,28 @@ module.exports = withMatrixClient(React.createClass({
});
},
onIgnoreToggle: function() {
const ignoredUsers = this.props.matrixClient.getIgnoredUsers();
if (this.state.isIgnoring) {
const index = ignoredUsers.indexOf(this.props.member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
} else {
ignoredUsers.push(this.props.member.userId);
}
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => {
return this.setState({isIgnoring: !this.state.isIgnoring});
});
},
onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
member: this.props.member,
action: kickLabel,
askReason: membership == "join",
action: membership === "invite" ? _t("Disinvite") : _t("Kick"),
title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"),
askReason: membership === "join",
danger: true,
onFinished: (proceed, reason) => {
if (!proceed) return;
@ -239,7 +268,7 @@ module.exports = withMatrixClient(React.createClass({
this.setState({ updating: this.state.updating + 1 });
this.props.matrixClient.kick(
this.props.member.roomId, this.props.member.userId,
reason || undefined
reason || undefined,
).then(function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
@ -251,11 +280,11 @@ module.exports = withMatrixClient(React.createClass({
title: _t("Failed to kick"),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
}
},
});
},
@ -263,22 +292,23 @@ module.exports = withMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
member: this.props.member,
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
askReason: this.props.member.membership != 'ban',
danger: this.props.member.membership != 'ban',
action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"),
title: this.props.member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"),
askReason: this.props.member.membership !== 'ban',
danger: this.props.member.membership !== 'ban',
onFinished: (proceed, reason) => {
if (!proceed) return;
this.setState({ updating: this.state.updating + 1 });
let promise;
if (this.props.member.membership == 'ban') {
if (this.props.member.membership === 'ban') {
promise = this.props.matrixClient.unban(
this.props.member.roomId, this.props.member.userId,
);
} else {
promise = this.props.matrixClient.ban(
this.props.member.roomId, this.props.member.userId,
reason || undefined
reason || undefined,
);
}
promise.then(
@ -293,7 +323,7 @@ module.exports = withMatrixClient(React.createClass({
title: _t("Error"),
description: _t("Failed to ban user"),
});
}
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
@ -302,35 +332,30 @@ module.exports = withMatrixClient(React.createClass({
},
onMuteToggle: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = this.props.matrixClient.getRoom(roomId);
if (!room) {
return;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevelEvent) {
return;
}
var isMuted = this.state.muted;
var powerLevels = powerLevelEvent.getContent();
var levelToSend = (
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
const isMuted = this.state.muted;
const powerLevels = powerLevelEvent.getContent();
const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
var level;
let level;
if (isMuted) { // unmute
level = levelToSend;
}
else { // mute
} else { // mute
level = levelToSend - 1;
}
level = parseInt(level);
if (level !== NaN) {
if (!isNaN(level)) {
this.setState({ updating: this.state.updating + 1 });
this.props.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then(
function() {
@ -343,7 +368,7 @@ module.exports = withMatrixClient(React.createClass({
title: _t("Error"),
description: _t("Failed to mute user"),
});
}
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
@ -351,28 +376,23 @@ module.exports = withMatrixClient(React.createClass({
},
onModToggle: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = this.props.matrixClient.getRoom(roomId);
if (!room) {
return;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevelEvent) {
return;
}
var me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) {
return;
}
var defaultLevel = powerLevelEvent.getContent().users_default;
var modLevel = me.powerLevel - 1;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
const me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) return;
const defaultLevel = powerLevelEvent.getContent().users_default;
let modLevel = me.powerLevel - 1;
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
// toggle the level
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
const newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
this.setState({ updating: this.state.updating + 1 });
this.props.matrixClient.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
function() {
@ -380,7 +400,7 @@ module.exports = withMatrixClient(React.createClass({
// get out of sync if we force setState here!
console.log("Mod toggle success");
}, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'});
} else {
console.error("Toggle moderator error:" + err);
@ -389,7 +409,7 @@ module.exports = withMatrixClient(React.createClass({
description: _t("Failed to toggle moderator status"),
});
}
}
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
@ -409,36 +429,35 @@ module.exports = withMatrixClient(React.createClass({
title: _t("Error"),
description: _t("Failed to change power level"),
});
}
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}).done();
},
onPowerChange: function(powerLevel) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = this.props.matrixClient.getRoom(roomId);
var self = this;
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;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
if (!powerLevelEvent) {
return;
}
if (powerLevelEvent.getContent().users) {
var myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
const myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
if (parseInt(myPower) === parseInt(powerLevel)) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
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("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"),
@ -448,12 +467,10 @@ module.exports = withMatrixClient(React.createClass({
}
},
});
}
else {
} else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
}
else {
} else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
@ -476,40 +493,36 @@ module.exports = withMatrixClient(React.createClass({
const defaultPerms = {
can: {},
muted: false,
modifyLevel: false
};
const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) {
return defaultPerms;
}
const powerLevels = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevels) {
return defaultPerms;
}
if (!room) return defaultPerms;
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevels) return defaultPerms;
const me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) {
return defaultPerms;
}
if (!me) return defaultPerms;
const them = member;
return {
can: this._calculateCanPermissions(
me, them, powerLevels.getContent()
me, them, powerLevels.getContent(),
),
muted: this._isMuted(them, powerLevels.getContent()),
isTargetMod: them.powerLevel > powerLevels.getContent().users_default
isTargetMod: them.powerLevel > powerLevels.getContent().users_default,
};
},
_calculateCanPermissions: function(me, them, powerLevels) {
const isMe = me.userId === them.userId;
const can = {
kick: false,
ban: false,
mute: false,
modifyLevel: false
modifyLevel: false,
modifyLevelMax: 0,
};
const canAffectUser = them.powerLevel < me.powerLevel;
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can;
@ -518,24 +531,20 @@ module.exports = withMatrixClient(React.createClass({
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
);
const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
can.modifyLevel = me.powerLevel > them.powerLevel;
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevelMax = me.powerLevel;
return can;
},
_isMuted: function(member, powerLevelContent) {
if (!powerLevelContent || !member) {
return false;
}
var levelToSend = (
if (!powerLevelContent || !member) return false;
const levelToSend = (
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default
);
@ -545,19 +554,20 @@ module.exports = withMatrixClient(React.createClass({
onCancel: function(e) {
dis.dispatch({
action: "view_user",
member: null
member: null,
});
},
onMemberAvatarClick: function() {
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
if(!avatarUrl) return;
const member = this.props.member;
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
if (!avatarUrl) return;
var httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
src: httpUrl,
name: this.props.member.name
name: member.name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
@ -571,15 +581,13 @@ module.exports = withMatrixClient(React.createClass({
},
_renderDevices: function() {
if (!this._enableDevices) {
return null;
}
if (!this._enableDevices) return null;
var devices = this.state.devices;
var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
var Spinner = sdk.getComponent("elements.Spinner");
const devices = this.state.devices;
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
const Spinner = sdk.getComponent("elements.Spinner");
var devComponents;
let devComponents;
if (this.state.devicesLoading) {
// still loading
devComponents = <Spinner />;
@ -589,10 +597,10 @@ module.exports = withMatrixClient(React.createClass({
devComponents = _t("No devices with registered encryption keys");
} else {
devComponents = [];
for (var i = 0; i < devices.length; i++) {
for (let i = 0; i < devices.length; i++) {
devComponents.push(<MemberDeviceInfo key={i}
userId={this.props.member.userId}
device={devices[i]}/>);
device={devices[i]} />);
}
}
@ -600,14 +608,108 @@ module.exports = withMatrixClient(React.createClass({
<div>
<h3>{ _t("Devices") }</h3>
<div className="mx_MemberInfo_devices">
{devComponents}
{ devComponents }
</div>
</div>
);
},
_renderUserOptions: function() {
const cli = this.props.matrixClient;
const member = this.props.member;
let ignoreButton = null;
let insertPillButton = null;
let inviteUserButton = null;
let readReceiptButton = null;
// Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt
if (member.userId !== cli.getUserId()) {
ignoreButton = (
<AccessibleButton onClick={this.onIgnoreToggle} className="mx_MemberInfo_field">
{ this.state.isIgnoring ? _t("Unignore") : _t("Ignore") }
</AccessibleButton>
);
if (member.roomId) {
const room = cli.getRoom(member.roomId);
const eventId = room.getEventReadUpTo(member.userId);
const onReadReceiptButton = function() {
dis.dispatch({
action: 'view_room',
highlighted: true,
event_id: eventId,
room_id: member.roomId,
});
};
const onInsertPillButton = function() {
dis.dispatch({
action: 'insert_mention',
user_id: member.userId,
});
};
readReceiptButton = (
<AccessibleButton onClick={onReadReceiptButton} className="mx_MemberInfo_field">
{ _t('Jump to read receipt') }
</AccessibleButton>
);
insertPillButton = (
<AccessibleButton onClick={onInsertPillButton} className={"mx_MemberInfo_field"}>
{ _t('Mention') }
</AccessibleButton>
);
}
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async () => {
try {
await cli.invite(roomId, member.userId);
} catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t('Failed to invite'),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
};
inviteUserButton = (
<AccessibleButton onClick={onInviteUserButton} className="mx_MemberInfo_field">
{ _t('Invite') }
</AccessibleButton>
);
}
}
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
return (
<div>
<h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons">
{ readReceiptButton }
{ insertPillButton }
{ ignoreButton }
{ inviteUserButton }
</div>
</div>
);
},
render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
let startChat;
let kickButton;
let banButton;
let muteButton;
let giveModButton;
let spinner;
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.
@ -623,6 +725,7 @@ module.exports = withMatrixClient(React.createClass({
const room = this.props.matrixClient.getRoom(roomId);
if (room) {
const me = room.getMember(this.props.matrixClient.credentials.userId);
// 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
@ -639,7 +742,7 @@ module.exports = withMatrixClient(React.createClass({
highlight={highlight}
isInvite={me.membership === "invite"}
onClick={this.onRoomTileClick}
/>
/>,
);
}
}
@ -660,14 +763,14 @@ module.exports = withMatrixClient(React.createClass({
startChat = <div>
<h3>{ _t("Direct chats") }</h3>
{tiles}
{startNewChat}
{ tiles }
{ startNewChat }
</div>;
}
if (this.state.updating) {
var Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
const Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
}
if (this.state.can.kick) {
@ -676,19 +779,19 @@ module.exports = withMatrixClient(React.createClass({
kickButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this.onKick}>
{kickLabel}
{ kickLabel }
</AccessibleButton>
);
}
if (this.state.can.ban) {
let label = _t("Ban");
if (this.props.member.membership == 'ban') {
if (this.props.member.membership === 'ban') {
label = _t("Unban");
}
banButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this.onBanOrUnban}>
{label}
{ label }
</AccessibleButton>
);
}
@ -697,72 +800,92 @@ module.exports = withMatrixClient(React.createClass({
muteButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this.onMuteToggle}>
{muteLabel}
{ muteLabel }
</AccessibleButton>
);
}
if (this.state.can.toggleMod) {
var giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
const giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
{ giveOpLabel }
</AccessibleButton>;
}
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
// e.g. clicking on a linkified userid in a room
var adminTools;
let adminTools;
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
<div>
<h3>{_t("Admin tools")}</h3>
<h3>{ _t("Admin Tools") }</h3>
<div className="mx_MemberInfo_buttons">
{muteButton}
{kickButton}
{banButton}
{giveModButton}
{ muteButton }
{ kickButton }
{ banButton }
{ giveModButton }
</div>
</div>;
}
const memberName = this.props.member.name;
let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
if (this.props.member.user) {
var presenceState = this.props.member.user.presence;
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
var presenceLastTs = this.props.member.user.lastPresenceTs;
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
presenceState = this.props.member.user.presence;
presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
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;
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>
<PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)}
maxValue={this.state.can.modifyLevelMax}
disabled={!this.state.can.modifyLevel}
usersDefault={powerLevelUsersDefault}
onChange={this.onPowerChange} />
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
</div>
</div>;
}
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
<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} />
</div>
<EmojiText element="h2">{memberName}</EmojiText>
<EmojiText element="h2">{ memberName }</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div>
{ roomMemberDetails }
</div>
{ this._renderUserOptions() }
{ adminTools }
{ startChat }
@ -773,5 +896,5 @@ module.exports = withMatrixClient(React.createClass({
</GeminiScrollbar>
</div>
);
}
},
}));

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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.
@ -14,46 +15,41 @@ 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.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
import Promise from 'bluebird';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var Entities = require("../../../Entities");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
var rate_limited_func = require('../../../ratelimitedfunc');
var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30;
import React from 'react';
import { _t } from '../../../languageHandler';
const MatrixClientPeg = require("../../../MatrixClientPeg");
const sdk = require('../../../index');
const GeminiScrollbar = require('react-gemini-scrollbar');
const rate_limited_func = require('../../../ratelimitedfunc');
const CallHandler = require("../../../CallHandler");
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
const SHOW_MORE_INCREMENT = 100;
module.exports = React.createClass({
displayName: 'MemberList',
getInitialState: function() {
var state = {
members: [],
this.memberDict = this.getMemberDict();
const members = this.roomMembers();
return {
members: members,
filteredJoinedMembers: this._filterMembers(members, 'join'),
filteredInvitedMembers: this._filterMembers(members, 'invite'),
// ideally we'd size this to the page height, but
// in practice I find that a little constraining
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
searchQuery: "",
};
if (!this.props.roomId) return state;
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (!room) return state;
this.memberDict = this.getMemberDict();
state.members = this.roomMembers();
return state;
},
componentWillMount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent);
@ -66,7 +62,7 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("RoomMember.name", this.onRoomMemberName);
@ -113,7 +109,7 @@ module.exports = React.createClass({
// member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener.
// console.log("explicit presence from " + user.userId);
var tile = this.refs[user.userId];
const tile = this.refs[user.userId];
if (tile) {
this._updateList(); // reorder the membership list
}
@ -147,19 +143,21 @@ module.exports = React.createClass({
// console.log("Updating memberlist");
this.memberDict = this.getMemberDict();
var self = this;
this.setState({
members: self.roomMembers()
});
const newState = {
members: this.roomMembers(),
};
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
this.setState(newState);
}, 500),
getMemberDict: function() {
if (!this.props.roomId) return {};
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
if (!room) return {};
var all_members = room.currentState.members;
const all_members = room.currentState.members;
Object.keys(all_members).map(function(userId) {
// work around a race where you might have a room member object
@ -177,19 +175,19 @@ module.exports = React.createClass({
},
roomMembers: function() {
var all_members = this.memberDict || {};
var all_user_ids = Object.keys(all_members);
var ConferenceHandler = CallHandler.getConferenceHandler();
const all_members = this.memberDict || {};
const all_user_ids = Object.keys(all_members);
const ConferenceHandler = CallHandler.getConferenceHandler();
all_user_ids.sort(this.memberSort);
var to_display = [];
var count = 0;
for (var i = 0; i < all_user_ids.length; ++i) {
var user_id = all_user_ids[i];
var m = all_members[user_id];
const to_display = [];
let count = 0;
for (let i = 0; i < all_user_ids.length; ++i) {
const user_id = all_user_ids[i];
const m = all_members[user_id];
if (m.membership == 'join' || m.membership == 'invite') {
if (m.membership === 'join' || m.membership === 'invite') {
if ((ConferenceHandler && !ConferenceHandler.isConferenceUser(user_id)) || !ConferenceHandler) {
to_display.push(user_id);
++count;
@ -199,7 +197,15 @@ module.exports = React.createClass({
return to_display;
},
_createOverflowTile: function(overflowCount, totalCount) {
_createOverflowTileJoined: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
},
_createOverflowTileInvited: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
},
_createOverflowTile: function(overflowCount, totalCount, onClick) {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -208,21 +214,26 @@ module.exports = React.createClass({
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullMemberList} />
onClick={onClick} />
);
},
_showFullMemberList: function() {
_showMoreJoinedMemberList: function() {
this.setState({
truncateAt: -1
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
});
},
_showMoreInvitedMemberList: function() {
this.setState({
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
});
},
memberString: function(member) {
if (!member) {
return "(null)";
}
else {
} else {
return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")";
}
},
@ -236,10 +247,10 @@ module.exports = React.createClass({
// ...and then alphabetically.
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
var memberA = this.memberDict[userIdA];
var memberB = this.memberDict[userIdB];
var userA = memberA.user;
var userB = memberB.user;
const memberA = this.memberDict[userIdA];
const memberB = this.memberDict[userIdB];
const userA = memberA.user;
const userB = memberB.user;
// if (!userA || !userB) {
// console.log("comparing " + memberA.name + " user=" + memberA.user + " with " + memberB.name + " user=" + memberB.user);
@ -257,15 +268,13 @@ module.exports = React.createClass({
// console.log(memberA + " and " + memberB + " have same power level");
if (memberA.name && memberB.name) {
// console.log("comparing names: " + memberA.name + " and " + memberB.name);
var nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
var nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
return nameA.localeCompare(nameB);
}
else {
} else {
return 0;
}
}
else {
} else {
// console.log("comparing power: " + memberA.powerLevel + " and " + memberB.powerLevel);
return memberB.powerLevel - memberA.powerLevel;
}
@ -280,19 +289,20 @@ module.exports = React.createClass({
},
onSearchQueryChanged: function(ev) {
this.setState({ searchQuery: ev.target.value });
const q = ev.target.value;
this.setState({
searchQuery: q,
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q),
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q),
});
},
makeMemberTiles: function(membership, query) {
var MemberTile = sdk.getComponent("rooms.MemberTile");
query = (query || "").toLowerCase();
var self = this;
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
_filterMembers: function(members, membership, query) {
return members.filter((userId) => {
const m = this.memberDict[userId];
if (query) {
query = query.toLowerCase();
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
@ -301,39 +311,48 @@ module.exports = React.createClass({
}
}
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
return m.membership === membership;
});
},
_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} />
);
});
// XXX: surely this is not the right home for this logic.
// 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.
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)
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
var EntityTile = sdk.getComponent("rooms.EntityTile");
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
var required_keys = ['key_validity_url', 'public_key', 'display_name'];
for (var i = 0; i < required_keys.length; ++i) {
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.
var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />,
);
});
}
@ -342,40 +361,61 @@ module.exports = React.createClass({
return memberList;
},
_getChildrenJoined: function(start, end) {
return this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
},
_getChildCountJoined: function() {
return this.state.filteredJoinedMembers.length;
},
_getChildrenInvited: function(start, end) {
return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end), 'invite');
},
_getChildCountInvited: function() {
return this.state.filteredInvitedMembers.length;
},
render: function() {
var invitedSection = null;
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
if (invitedMemberTiles.length > 0) {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
let invitedSection = null;
if (this._getChildCountInvited() > 0) {
invitedSection = (
<div className="mx_MemberList_invited">
<h2>{ _t("Invited") }</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtInvited}
createOverflowElement={this._createOverflowTileInvited}
getChildren={this._getChildrenInvited}
getChildCount={this._getChildCountInvited}
/>
</div>
</div>
);
}
var inputBox = (
const inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter room members') } />
placeholder={_t('Filter room members')} />
</form>
);
var TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{this.makeMemberTiles('join', this.state.searchQuery)}
</TruncatedList>
{invitedSection}
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined}
/>
{ invitedSection }
</GeminiScrollbar>
</div>
);
}
},
});

View file

@ -16,12 +16,12 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
const dis = require('../../../dispatcher');
const Modal = require("../../../Modal");
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -68,16 +68,16 @@ module.exports = React.createClass({
},
render: function() {
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
var EntityTile = sdk.getComponent('rooms.EntityTile');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
var member = this.props.member;
var name = this._getDisplayName();
var active = -1;
var presenceState = member.user ? member.user.presence : null;
const member = this.props.member;
const name = this._getDisplayName();
const active = -1;
const presenceState = member.user ? member.user.presence : null;
var av = (
const av = (
<MemberAvatar member={member} width={36} height={36} />
);
@ -86,13 +86,19 @@ module.exports = React.createClass({
}
this.member_last_modified_time = member.getLastModifiedTime();
// We deliberately leave power levels that are not 100 or 50 undefined
const powerStatus = {
100: EntityTile.POWER_STATUS_ADMIN,
50: EntityTile.POWER_STATUS_MODERATOR,
}[this.props.member.powerLevel];
return (
<EntityTile {...this.props} presenceState={presenceState}
presenceLastActiveAgo={ member.user ? member.user.lastActiveAgo : 0 }
presenceLastTs={ member.user ? member.user.lastPresenceTs : 0 }
presenceCurrentlyActive={ member.user ? member.user.currentlyActive : false }
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerLevel={this.props.member.powerLevel} />
name={name} powerStatus={powerStatus} />
);
}
},
});

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.
@ -21,7 +22,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class MessageComposer extends React.Component {
@ -48,10 +49,10 @@ export default class MessageComposer extends React.Component {
inputState: {
style: [],
blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
wordCount: 0,
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
};
}
@ -89,13 +90,13 @@ export default class MessageComposer extends React.Component {
}
uploadFiles(files) {
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileList = [];
const fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || _t('Attachment')}
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') }
</li>);
}
@ -105,15 +106,15 @@ export default class MessageComposer extends React.Component {
<div>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{fileList}
{ fileList }
</ul>
</div>
),
onFinished: (shouldUpload) => {
if(shouldUpload) {
if (shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) {
for(let i=0; i<files.length; i++) {
for (let i=0; i<files.length; i++) {
this.props.uploadFile(files[i]);
}
}
@ -225,7 +226,7 @@ export default class MessageComposer extends React.Component {
}
onToggleFormattingClicked() {
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
this.setState({showFormatting: !this.state.showFormatting});
}
@ -237,7 +238,7 @@ export default class MessageComposer extends React.Component {
render() {
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
@ -245,7 +246,7 @@ export default class MessageComposer extends React.Component {
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
<MemberPresenceAvatar member={me} width={24} height={24} />
</div>,
);
@ -271,32 +272,30 @@ export default class MessageComposer extends React.Component {
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
<img src="img/hangup.svg" alt={_t('Hangup')} title={_t('Hangup')} width="25" height="26" />
</div>;
} else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
<TintableSvg src="img/icon-call.svg" width="35" height="35" />
</div>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={ _t('Video call') }>
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
<TintableSvg src="img/icons-video.svg" width="35" height="35" />
</div>;
}
// Apps
if (UserSettingsStore.isFeatureEnabled('matrix_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>;
}
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(
@ -308,8 +307,8 @@ export default class MessageComposer extends React.Component {
// complex because of conference calls.
const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title={ _t('Upload file') }>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
onClick={this.onUploadClick} title={_t('Upload file')}>
<TintableSvg src="img/icons-upload.svg" width="35" height="35" />
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
@ -363,7 +362,7 @@ export default class MessageComposer extends React.Component {
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={ _t(name) }
title={_t(name)}
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
@ -372,21 +371,21 @@ export default class MessageComposer extends React.Component {
);
return (
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
<div className="mx_MessageComposer">
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{controls}
{ controls }
</div>
</div>
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons}
{ formatButtons }
<div style={{flex: 1}}></div>
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
<img title={this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title={ _t("Hide Text Formatting Toolbar") }
<img title={_t("Hide Text Formatting Toolbar")}
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
@ -411,9 +410,6 @@ MessageComposer.propTypes = {
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// string representing the current room app drawer state
showApps: React.PropTypes.bool,
};

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.
@ -27,14 +28,13 @@ import Promise from 'bluebird';
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
@ -49,6 +49,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
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";
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@ -57,6 +58,11 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL',
};
function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
@ -68,13 +74,6 @@ function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({
action: 'message_send_failed',
});
@ -99,13 +98,7 @@ export default class MessageComposerInput extends React.Component {
};
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
@ -159,7 +152,7 @@ export default class MessageComposerInput extends React.Component {
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
this.onTextPasted = this.onTextPasted.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
Analytics.setRichtextMode(isRichtextEnabled);
@ -187,13 +180,16 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
(
contentState.getEntity(entityKey).getType() === 'LINK' ||
contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL
)
);
}, callback,
);
@ -207,13 +203,21 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
decorators.push({
strategy: this.findLinkEntities.bind(this),
strategy: this.findPillEntities.bind(this),
component: (entityProps) => {
const Pill = sdk.getComponent('elements.Pill');
const type = entityProps.contentState.getEntity(entityProps.entityKey).getType();
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
if (Pill.isPillUrl(url)) {
if (type === ENTITY_TYPES.AT_ROOM_PILL) {
return <Pill
type={Pill.TYPE_AT_ROOM_MENTION}
room={this.props.room}
offsetKey={entityProps.offsetKey}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
} else if (Pill.isPillUrl(url)) {
return <Pill
url={url}
room={this.props.room}
@ -224,7 +228,7 @@ export default class MessageComposerInput extends React.Component {
return (
<a href={url} data-offset-key={entityProps.offsetKey}>
{entityProps.children}
{ entityProps.children }
</a>
);
},
@ -286,7 +290,7 @@ export default class MessageComposerInput extends React.Component {
/// 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.
let body = escape(payload.text);
const body = escape(payload.text);
if (body) {
let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`);
if (!this.state.isRichtextEnabled) {
@ -367,7 +371,7 @@ export default class MessageComposerInput extends React.Component {
}
sendTyping(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
if (SettingsStore.getValue('dontSendTypingNotifications')) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT,
@ -414,10 +418,10 @@ export default class MessageComposerInput extends React.Component {
}
// Automatic replacement of plaintext emoji to Unicode emoji
if (UserSettingsStore.getSyncedSetting('MessageComposerInput.autoReplaceEmoji', false)) {
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if(emojiMatch) {
if (emojiMatch) {
// plaintext -> hex unicode
const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode
@ -464,7 +468,7 @@ export default class MessageComposerInput extends React.Component {
// autocomplete will probably have different completions to show.
if (
!state.editorState.getSelection().equals(
this.state.editorState.getSelection()
this.state.editorState.getSelection(),
)
&& state.editorState.getCurrentContent().getPlainText() ===
this.state.editorState.getCurrentContent().getPlainText()
@ -508,7 +512,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;
}
});
}
@ -534,7 +539,7 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, contentState),
isRichtextEnabled: enabled,
});
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
}
handleKeyCommand = (command: string): boolean => {
@ -679,7 +684,7 @@ export default class MessageComposerInput extends React.Component {
}
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
if(
if (
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
.includes(currentBlockType)
) {
@ -783,7 +788,7 @@ export default class MessageComposerInput extends React.Component {
const pt = contentState.getBlocksAsArray().map((block) => {
let blockText = block.getText();
let offset = 0;
this.findLinkEntities(contentState, block, (start, end) => {
this.findPillEntities(contentState, block, (start, end) => {
const entity = contentState.getEntity(block.getEntityAt(start));
if (entity.getType() !== 'LINK') {
return;
@ -974,7 +979,6 @@ export default class MessageComposerInput extends React.Component {
editorState = EditorState.forceSelection(editorState,
editorState.getSelection());
this.setState({editorState});
}
return false;
}
@ -989,6 +993,11 @@ export default class MessageComposerInput extends React.Component {
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
} else if (completion === '@room') {
contentState = contentState.createEntity(ENTITY_TYPES.AT_ROOM_PILL, 'IMMUTABLE', {
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
}
let selection;
@ -1032,10 +1041,10 @@ export default class MessageComposerInput extends React.Component {
buttons. */
getSelectionInfo(editorState: EditorState) {
const styleName = {
BOLD: 'bold',
ITALIC: 'italic',
STRIKETHROUGH: 'strike',
UNDERLINE: 'underline',
BOLD: _td('bold'),
ITALIC: _td('italic'),
STRIKETHROUGH: _td('strike'),
UNDERLINE: _td('underline'),
};
const originalStyle = editorState.getCurrentInlineStyle().toArray();
@ -1044,10 +1053,10 @@ export default class MessageComposerInput extends React.Component {
.filter((styleName) => !!styleName);
const blockName = {
'code-block': 'code',
'blockquote': 'quote',
'unordered-list-item': 'bullet',
'ordered-list-item': 'numbullet',
'code-block': _td('code'),
'blockquote': _td('quote'),
'unordered-list-item': _td('bullet'),
'ordered-list-item': _td('numbullet'),
};
const originalBlockType = editorState.getCurrentContent()
.getBlockForKey(editorState.getSelection().getStartKey())
@ -1131,15 +1140,17 @@ export default class MessageComposerInput extends React.Component {
<div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}
onConfirm={this.setDisplayedCompletion}
onSelectionChange={this.setDisplayedCompletion}
query={this.getAutocompleteQuery(content)}
selection={selection}/>
selection={selection}
/>
</div>
<div className={className}>
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
onMouseDown={this.onMarkdownToggleClicked}
title={ this.state.isRichtextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
title={this.state.isRichtextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
<Editor ref="editor"
dir="auto"
@ -1157,7 +1168,7 @@ export default class MessageComposerInput extends React.Component {
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onEscape={this.onEscape}
spellCheck={true}/>
spellCheck={true} />
</div>
</div>
);

View file

@ -0,0 +1,90 @@
/*
Copyright 2017 Travis Ralston
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 MatrixClientPeg from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
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,
},
onTileClicked: function() {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
onUnpinClicked: function() {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
},
_canUnpin: function() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
},
render: function() {
const sender = this.props.mxRoom.getMember(this.props.mxEvent.getSender());
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src="img/cancel-red.svg" width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
<span className="mx_PinnedEventTile_sender">
{ sender.name }
</span>
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
</div>
);
},
});

View file

@ -0,0 +1,127 @@
/*
Copyright 2017 Travis Ralston
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 MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
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,
onCancelClick: React.PropTypes.func,
},
getInitialState: function() {
return {
loading: true,
};
},
componentDidMount: function() {
this._updatePinnedMessages();
},
_updatePinnedMessages: function() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event};
}).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
this._updateReadState();
},
_updateReadState: function() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
let readStateEvents = [];
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
readStateEvents = readPinsEvent.getContent().event_ids || [];
}
if (!readStateEvents.includes(pinnedEvents.getId())) {
readStateEvents.push(pinnedEvents.getId());
// Only keep the last 10 event IDs to avoid infinite growth
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
event_ids: readStateEvents,
});
}
},
_getPinnedTiles: function() {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map((context) => {
return (<PinnedEventTile key={context.event.getId()}
mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages} />);
});
},
render: function() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>
</div>
);
},
});

View file

@ -34,61 +34,60 @@ module.exports = React.createClass({
currentlyActive: React.PropTypes.bool,
// offline, online, etc
presenceState: React.PropTypes.string
presenceState: React.PropTypes.string,
},
getDefaultProps: function() {
return {
ago: -1,
presenceState: null
presenceState: null,
};
},
// Return duration as a string using appropriate time units
// XXX: This would be better handled using a culture-aware library, but we don't use one yet.
getDuration: function(time) {
if (!time) return;
var t = parseInt(time / 1000);
var s = t % 60;
var m = parseInt(t / 60) % 60;
var h = parseInt(t / (60 * 60)) % 24;
var d = parseInt(t / (60 * 60 * 24));
const t = parseInt(time / 1000);
const s = t % 60;
const m = parseInt(t / 60) % 60;
const h = parseInt(t / (60 * 60)) % 24;
const d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return _t("for %(amount)ss", {amount: 0});
return _t("%(duration)ss", {duration: 0});
}
return _t("for %(amount)ss", {amount: s});
return _t("%(duration)ss", {duration: s});
}
if (t < 60 * 60) {
return _t("for %(amount)sm", {amount: m});
return _t("%(duration)sm", {duration: m});
}
if (t < 24 * 60 * 60) {
return _t("for %(amount)sh", {amount: h});
return _t("%(duration)sh", {duration: h});
}
return _t("for %(amount)sd", {amount: d});
return _t("%(duration)sd", {duration: d});
},
getPrettyPresence: function(presence) {
if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline");
return "Unknown";
getPrettyPresence: function(presence, activeAgo, currentlyActive) {
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo);
if (presence === "online") return _t("Online for %(duration)s", { duration: duration });
if (presence === "unavailable") return _t("Idle for %(duration)s", { duration: duration }); // XXX: is this actually right?
if (presence === "offline") return _t("Offline for %(duration)s", { duration: duration });
return _t("Unknown for %(duration)s", { duration: duration });
} else {
if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline");
return _t("Unknown");
}
},
render: function() {
if (this.props.activeAgo >= 0) {
let duration = this.getDuration(this.props.activeAgo);
let ago = this.props.currentlyActive || !duration ? "" : duration;
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { ago }
</div>
);
}
else {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) }
</div>
);
}
}
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
</div>
);
},
});

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
const React = require('react');
const ReactDOM = require('react-dom');
var sdk = require('../../../index');
const sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor');
const Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
import { _t } from '../../../languageHandler';
import DateUtils from '../../../DateUtils';
var bounce = false;
let bounce = false;
try {
if (global.localStorage) {
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
@ -90,7 +90,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
// before we remove the rr, store its location in the map, so that if
// it reappears, it can be animated from the right place.
var rrInfo = this.props.readReceiptInfo;
const rrInfo = this.props.readReceiptInfo;
if (!rrInfo) {
return;
}
@ -102,7 +102,7 @@ module.exports = React.createClass({
return;
}
var avatarNode = ReactDOM.findDOMNode(this);
const avatarNode = ReactDOM.findDOMNode(this);
rrInfo.top = avatarNode.offsetTop;
rrInfo.left = avatarNode.offsetLeft;
rrInfo.parent = avatarNode.offsetParent;
@ -115,29 +115,40 @@ module.exports = React.createClass({
}
// treat new RRs as though they were off the top of the screen
var oldTop = -15;
let oldTop = -15;
var oldInfo = this.props.readReceiptInfo;
const oldInfo = this.props.readReceiptInfo;
if (oldInfo && oldInfo.parent) {
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
}
var newElement = ReactDOM.findDOMNode(this);
var startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
const newElement = ReactDOM.findDOMNode(this);
let startTopOffset;
if (!newElement.offsetParent) {
// this seems to happen sometimes for reasons I don't understand
// the docs for `offsetParent` say it may be null if `display` is
// `none`, but I can't see why that would happen.
console.warn(
`ReadReceiptMarker for ${this.props.member.userId} in ` +
`${this.props.member.roomId} has no offsetParent`,
);
startTopOffset = 0;
} else {
startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
}
var startStyles = [];
var enterTransitionOpts = [];
const startStyles = [];
const enterTransitionOpts = [];
if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos
var leftOffset = oldInfo.left;
startStyles.push({ top: startTopOffset+"px",
left: oldInfo.left+"px" });
var reorderTransitionOpts = {
const reorderTransitionOpts = {
duration: 100,
easing: 'easeOut'
easing: 'easeOut',
};
enterTransitionOpts.push(reorderTransitionOpts);
@ -160,12 +171,12 @@ module.exports = React.createClass({
render: function() {
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
if (this.state.suppressDisplay) {
return <div/>;
return <div />;
}
var style = {
const style = {
left: this.props.leftOffset+'px',
top: '0px',
visibility: this.props.hidden ? 'hidden' : 'visible',
@ -175,7 +186,7 @@ module.exports = React.createClass({
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)}
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)},
);
}

View file

@ -0,0 +1,148 @@
/*
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 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>;
},
});
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,
})),
className: PropTypes.string,
},
getRows: function() {
if (!this.props.rooms) return [];
return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} />;
});
},
render() {
const rows = this.getRows();
let rooms;
if (rows.length == 0) {
rooms = <i>{ _t('No rooms to show') }</i>;
} else {
rooms = <table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
</tbody>
</table>;
}
return <div className={classNames("mx_RoomDetailList", this.props.className)}>
{ rooms }
</div>;
},
});

View file

@ -31,6 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
linkifyMatrix(linkify);
@ -45,6 +46,7 @@ module.exports = React.createClass({
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,
@ -56,13 +58,14 @@ module.exports = React.createClass({
editing: false,
inRoom: false,
onSaveClick: function() {},
onCancelClick: function() {},
onCancelClick: null,
};
},
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
// When a room name occurs, RoomState.events is fired *before*
// room.name is updated. So we have to listen to Room.name as well as
@ -85,6 +88,7 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
}
},
@ -97,6 +101,13 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
_onRoomAccountData: function(event, room) {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
},
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
@ -129,10 +140,40 @@ module.exports = React.createClass({
}).done();
},
onAvatarRemoveClick: function() {
MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, '');
},
onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
_hasUnreadPins: function() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
return false; // no pins == nothing to read
}
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
const readStateEvents = readPinsEvent.getContent().event_ids || [];
if (readStateEvents) {
return !readStateEvents.includes(currentPinEvent.getId());
}
}
// There's pins, and we haven't read any of them
return true;
},
_hasPins: function() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
},
/**
* After editing the settings, get the new name for the room
*
@ -172,6 +213,7 @@ module.exports = React.createClass({
let spinner = null;
let saveButton = null;
let settingsButton = null;
let pinnedEventsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
@ -186,18 +228,18 @@ module.exports = React.createClass({
saveButton = (
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
{_t("Save")}
{ _t("Save") }
</AccessibleButton>
);
}
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.saving) {
const Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
}
if (canSetRoomName) {
@ -254,7 +296,7 @@ module.exports = React.createClass({
}
if (topic) {
topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
}
}
@ -262,16 +304,23 @@ module.exports = React.createClass({
if (canSetRoomAvatar) {
roomAvatar = (
<div className="mx_RoomHeader_avatarPicker">
<div onClick={ this.onAvatarPickerClick }>
<div onClick={this.onAvatarPickerClick}>
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
</div>
<div className="mx_RoomHeader_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" />
alt={_t("Upload avatar")} title={_t("Upload avatar")}
width="17" height="15" />
</label>
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
</div>
<div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
<img src="img/cancel.svg"
className="mx_filterFlipColor"
width="10"
alt={_t("Remove avatar")}
title={_t("Remove avatar")} />
</div>
</div>
);
@ -286,7 +335,23 @@ module.exports = React.createClass({
if (this.props.onSettingsClick) {
settingsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
</AccessibleButton>;
}
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
} else if (this._hasPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
}
pinnedEventsButton =
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
{ pinsIndicator }
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
</AccessibleButton>;
}
@ -301,30 +366,30 @@ module.exports = React.createClass({
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={_t("Forget room")}>
<TintableSvg src="img/leave.svg" width="26" height="20" />
</AccessibleButton>;
}
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
<TintableSvg src="img/icons-search.svg" width="35" height="35" />
</AccessibleButton>;
}
let rightPanelButtons;
if (this.props.collapsedRhs) {
rightPanelButtons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={_t('Show panel')}>
<TintableSvg src="img/maximise.svg" width="10" height="16" />
</AccessibleButton>;
}
let rightRow;
let manageIntegsButton;
if(this.props.room && this.props.room.roomId) {
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton
roomId={this.props.room.roomId}
/>;
@ -334,6 +399,7 @@ module.exports = React.createClass({
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settingsButton }
{ pinnedEventsButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }
@ -342,7 +408,7 @@ module.exports = React.createClass({
}
return (
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
<div className={"mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
@ -353,10 +419,10 @@ module.exports = React.createClass({
{ topicElement }
</div>
</div>
{spinner}
{saveButton}
{cancelButton}
{rightRow}
{ spinner }
{ saveButton }
{ cancelButton }
{ rightRow }
</div>
</div>
);

View file

@ -16,46 +16,37 @@ limitations under the License.
*/
'use strict';
var React = require("react");
var ReactDOM = require("react-dom");
import { _t, _tJsx } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter");
var Unread = require('../../../Unread');
var dis = require("../../../dispatcher");
var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms');
const React = require("react");
const ReactDOM = require("react-dom");
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 DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
const Receipt = require('../../../utils/Receipt');
import FilterStore from '../../../stores/FilterStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const HIDE_CONFERENCE_CHANS = true;
function phraseForSection(section) {
// These would probably be better as individual strings,
// but for some reason we have translations for these strings
// as-is, so keeping it like this for now.
let verb;
switch (section) {
case 'm.favourite':
verb = _t('to favourite');
break;
return _t('Drop here to favourite');
case 'im.vector.fake.direct':
verb = _t('to tag direct chat');
break;
return _t('Drop here to tag direct chat');
case 'im.vector.fake.recent':
verb = _t('to restore');
break;
return _t('Drop here to restore');
case 'm.lowpriority':
verb = _t('to demote');
break;
return _t('Drop here to demote');
default:
return _t('Drop here to tag %(section)s', {section: section});
}
return _t('Drop here %(toAction)s', {toAction: verb});
};
}
module.exports = React.createClass({
displayName: 'RoomList',
@ -63,7 +54,6 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
},
@ -73,13 +63,15 @@ module.exports = React.createClass({
totalRoomCount: null,
lists: {},
incomingCall: null,
selectedTags: [],
};
},
componentWillMount: function() {
this.mounted = false;
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.timeline", this.onRoomTimeline);
@ -88,7 +80,38 @@ module.exports = React.createClass({
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);
cli.on("Group.myMembership", this._onGroupMyMembership);
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 = [];
// When the selected tags are changed, initialise a group store if necessary
this._filterStoreToken = FilterStore.addListener(() => {
FilterStore.getSelectedTags().forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
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.refreshRoomList();
@ -97,7 +120,7 @@ module.exports = React.createClass({
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0
this._delayedRefreshRoomListLoopCount = 0;
},
componentDidMount: function() {
@ -123,13 +146,12 @@ module.exports = React.createClass({
var call = CallHandler.getCall(payload.room_id);
if (call && call.call_state === 'ringing') {
this.setState({
incomingCall: call
incomingCall: call,
});
this._repositionIncomingCallBox(undefined, true);
}
else {
} else {
this.setState({
incomingCall: null
incomingCall: null,
});
}
break;
@ -155,8 +177,20 @@ module.exports = React.createClass({
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._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
this._delayedRefreshRoomList.cancelPendingCall();
},
@ -171,7 +205,7 @@ module.exports = React.createClass({
onArchivedHeaderClick: function(isHidden, scrollToPosition) {
if (!isHidden) {
var self = this;
const self = this;
this.setState({ isLoadingLeftRooms: true });
// Try scrolling to position
@ -224,16 +258,64 @@ module.exports = React.createClass({
this._delayedRefreshRoomList();
},
onEventDecrypted: function(ev) {
// An event being decrypted may mean we need to re-order the room list
this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
this._delayedRefreshRoomList();
}
},
_onGroupMyMembership: function(group) {
this.forceUpdate();
},
_delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList();
}, 500),
// Update which rooms and users should appear in RoomList for a given group tag
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
const store = this._groupStores[tag];
if (!store) return;
this._visibleRoomsForGroup[tag] = [];
store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
store.getGroupMembers().forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
);
});
// TODO: Check if room has been tagged to the group by the user
},
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
this._visibleRooms = [];
FilterStore.getSelectedTags().forEach((tag) => {
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => this._visibleRooms.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);
},
refreshRoomList: function() {
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
@ -253,9 +335,7 @@ module.exports = React.createClass({
},
getRoomLists: function() {
var self = this;
const lists = {};
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
@ -263,9 +343,8 @@ module.exports = React.createClass({
lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) {
const dmRoomMap = DMRoomMap.shared();
MatrixClientPeg.get().getRooms().forEach((room) => {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
@ -276,35 +355,33 @@ module.exports = React.createClass({
if (me.membership == "invite") {
lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
} 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()))
{
} 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
var tagNames = Object.keys(room.tags);
const tagNames = Object.keys(room.tags);
// Apply TagPanel filtering, derived from FilterStore
if (!this.isRoomInSelectedTags(room)) {
return;
}
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
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)) {
} 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 {
} else {
lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
} else if (me.membership === "leave") {
lists["im.vector.fake.archived"].push(room);
}
else {
} else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
}
});
@ -331,7 +408,7 @@ module.exports = React.createClass({
_getScrollNode: function() {
if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this);
const panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
if (panel.classList.contains('gm-prevented')) {
@ -355,22 +432,22 @@ module.exports = React.createClass({
},
_repositionIncomingCallBox: function(e, firstTime) {
var incomingCallBox = document.getElementById("incomingCallBox");
const incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
var scrollArea = this._getScrollNode();
const scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
const scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
const scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
let top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
// Make sure we don't go too far up, if the headers aren't sticky
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
// make sure we don't go too far down, if the headers aren't sticky
var bottomMargin = scrollAreaOffset + (scrollAreaHeight - 45);
const bottomMargin = scrollAreaOffset + (scrollAreaHeight - 45);
top = (top > bottomMargin) ? bottomMargin : top;
incomingCallBox.style.top = top + "px";
@ -381,14 +458,14 @@ module.exports = React.createClass({
// Doing the sticky headers as raw DOM, for speed, as it gets very stuttery if done
// properly through React
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
var scrollArea = this._getScrollNode();
const scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
const scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
const scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
if (initialise) {
// Get a collection of sticky header containers references
@ -408,7 +485,7 @@ module.exports = React.createClass({
sticky.dataset.originalPosition = sticky.offsetTop - scrollArea.offsetTop;
// Save and set the sticky heights
var originalHeight = sticky.getBoundingClientRect().height;
const originalHeight = sticky.getBoundingClientRect().height;
sticky.dataset.originalHeight = originalHeight;
sticky.style.height = originalHeight;
@ -417,8 +494,8 @@ module.exports = React.createClass({
}
}
var self = this;
var scrollStuckOffset = 0;
const self = this;
let scrollStuckOffset = 0;
// Scroll to the passed in position, i.e. a header was clicked and in a scroll to state
// rather than a collapsable one (see RoomSubList.isCollapsableOnClick method for details)
if (scrollToPosition !== undefined) {
@ -426,11 +503,11 @@ module.exports = React.createClass({
}
// Stick headers to top and bottom, or free them
Array.prototype.forEach.call(this.stickies, function(sticky, i, stickyWrappers) {
var stickyPosition = sticky.dataset.originalPosition;
var stickyHeight = sticky.dataset.originalHeight;
var stickyHeader = sticky.childNodes[0];
var topStuckHeight = stickyHeight * i;
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i);
const stickyPosition = sticky.dataset.originalPosition;
const stickyHeight = sticky.dataset.originalHeight;
const stickyHeader = sticky.childNodes[0];
const topStuckHeight = stickyHeight * i;
const bottomStuckHeight = stickyHeight * (stickyWrappers.length - i);
if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) {
// Top stickies
@ -460,7 +537,7 @@ module.exports = React.createClass({
},
_updateStickyHeaders: function(initialise, scrollToPosition) {
var self = this;
const self = this;
if (initialise) {
// Useing setTimeout to ensure that the code is run after the painting
@ -481,6 +558,10 @@ module.exports = React.createClass({
},
_getEmptyContent: function(section) {
if (this.state.selectedTags.length > 0) {
return null;
}
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
@ -491,29 +572,26 @@ module.exports = React.createClass({
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
{_tJsx(
{ _t(
"Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/],
[
(sub) => <StartChatButton size="16" callout={true}/>
]
)}
{},
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
) }
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
{_tJsx(
{ _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
[
(sub) => <CreateRoomButton size="16" callout={true}/>,
(sub) => <RoomDirectoryButton size="16" callout={true}/>
]
)}
{},
{
'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
},
) }
</div>;
}
@ -544,112 +622,139 @@ module.exports = React.createClass({
}
},
_makeGroupInviteTiles() {
const ret = [];
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
for (const group of MatrixClientPeg.get().getGroups()) {
if (group.myMembership !== 'invite') continue;
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
}
return ret;
},
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label={ _t('Invites') }
editable={ false }
<RoomSubList list={[]}
extraTiles={this._makeGroupInviteTiles()}
label={_t('Community Invites')}
editable={false}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
isInvite={true}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
/>
<RoomSubList list={ self.state.lists['m.favourite'] }
label={ _t('Favourites') }
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
label={_t('Invites')}
editable={false}
order="recent"
isInvite={true}
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
/>
<RoomSubList list={self.state.lists['m.favourite']}
label={_t('Favourites')}
tagName="m.favourite"
emptyContent={this._getEmptyContent('m.favourite')}
editable={ true }
editable={true}
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label={ _t('People') }
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
label={_t('People')}
tagName="im.vector.fake.direct"
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={ true }
editable={true}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label={ _t('Rooms') }
editable={ true }
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
label={_t('Rooms')}
editable={true}
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 }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
{ Object.keys(self.state.lists).map((tagName) => {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName }
label={ tagName }
tagName={ tagName }
return <RoomSubList list={self.state.lists[tagName]}
key={tagName}
label={tagName}
tagName={tagName}
emptyContent={this._getEmptyContent(tagName)}
editable={ true }
editable={true}
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />;
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />;
}
}) }
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label={ _t('Low priority') }
<RoomSubList list={self.state.lists['m.lowpriority']}
label={_t('Low priority')}
tagName="m.lowpriority"
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={ true }
editable={true}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label={ _t('Historical') }
editable={ false }
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
label={_t('Historical')}
editable={false}
order="recent"
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms }
onHeaderClick= { self.onArchivedHeaderClick }
incomingCall={ self.state.incomingCall }
searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
selectedRoom={self.props.selectedRoom}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
startAsHidden={true}
showSpinner={self.state.isLoadingLeftRooms}
onHeaderClick= {self.onArchivedHeaderClick}
incomingCall={self.state.incomingCall}
searchFilter={self.props.searchFilter}
onShowMoreRooms={self.onShowMoreRooms} />
</div>
</GeminiScrollbar>
);
}
},
});

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
const React = require('react');
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -29,10 +29,10 @@ module.exports = React.createClass({
},
componentWillMount: function() {
var room = this.props.room;
var name = room.currentState.getStateEvents('m.room.name', '');
var myId = MatrixClientPeg.get().credentials.userId;
var defaultName = room.getDefaultRoomName(myId);
const room = this.props.room;
const name = room.currentState.getStateEvents('m.room.name', '');
const myId = MatrixClientPeg.get().credentials.userId;
const defaultName = room.getDefaultRoomName(myId);
this._initialName = name ? name.getContent().name : '';
@ -47,16 +47,16 @@ module.exports = React.createClass({
},
render: function() {
var EditableText = sdk.getComponent("elements.EditableText");
const EditableText = sdk.getComponent("elements.EditableText");
return (
<div className="mx_RoomHeader_name">
<EditableText ref="editor"
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={ this._placeholderName }
blurToCancel={ false }
initialValue={ this._initialName }
placeholder={this._placeholderName}
blurToCancel={false}
initialValue={this._initialName}
dir="auto" />
</div>
);

View file

@ -17,11 +17,11 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
const React = require('react');
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t, _tJsx } from '../../../languageHandler';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomPreviewBar',
@ -61,7 +61,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
busy: false
busy: false,
};
},
@ -72,7 +72,7 @@ module.exports = React.createClass({
if (this.props.inviterName && this.props.invitedEmail) {
this.setState({busy: true});
MatrixClientPeg.get().lookupThreePid(
'email', this.props.invitedEmail
'email', this.props.invitedEmail,
).finally(() => {
this.setState({busy: false});
}).done((result) => {
@ -83,17 +83,15 @@ module.exports = React.createClass({
}
},
_roomNameElement: function(fallback) {
fallback = fallback || _t('a room');
const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
return name ? name : fallback;
_roomNameElement: function() {
return this.props.room ? this.props.room.name : (this.props.room_alias || "");
},
render: function() {
var joinBlock, previewBlock;
let joinBlock, previewBlock;
if (this.props.spinner || this.state.busy) {
var Spinner = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_RoomPreviewBar">
<Spinner />
</div>);
@ -110,11 +108,11 @@ module.exports = React.createClass({
const banned = myMember && myMember.membership == 'ban';
if (this.props.inviterName) {
var emailMatchBlock;
let emailMatchBlock;
if (this.props.invitedEmail) {
if (this.state.threePidFetchError) {
emailMatchBlock = <div className="error">
{_t("Unable to ascertain that the address this invite was sent to matches one associated with your account.")}
{ _t("Unable to ascertain that the address this invite was sent to matches one associated with your account.") }
</div>;
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
emailMatchBlock =
@ -123,10 +121,10 @@ module.exports = React.createClass({
<img src="img/warning.svg" width="24" height="23" title= "/!\\" alt="/!\\" />
</div>
<div className="mx_RoomPreviewBar_warningText">
{_t("This invitation was sent to an email address which is not associated with this account:")}
<b><span className="email">{this.props.invitedEmail}</span></b>
<br/>
{_t("You may wish to login with a different account, or add this email to this account.")}
{ _t("This invitation was sent to an email address which is not associated with this account:") }
<b><span className="email">{ this.props.invitedEmail }</span></b>
<br />
{ _t("You may wish to login with a different account, or add this email to this account.") }
</div>
</div>;
}
@ -137,57 +135,63 @@ module.exports = React.createClass({
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
</div>
<div className="mx_RoomPreviewBar_join_text">
{ _tJsx(
{ _t(
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/],
[
(sub) => <a onClick={ this.props.onJoinClick }>{sub}</a>,
(sub) => <a onClick={ this.props.onRejectClick }>{sub}</a>
]
)}
{},
{
'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
},
) }
</div>
{emailMatchBlock}
{ emailMatchBlock }
</div>
);
} else if (kicked || banned) {
const roomName = this._roomNameElement(_t('This room'));
const roomName = this._roomNameElement();
const kickerMember = this.props.room.currentState.getMember(
myMember.events.member.getSender()
myMember.events.member.getSender(),
);
const kickerName = kickerMember ?
kickerMember.name : myMember.events.member.getSender();
let reason;
if (myMember.events.member.getContent().reason) {
reason = <div>{_t("Reason: %(reasonText)s", {reasonText: myMember.events.member.getContent().reason})}</div>
reason = <div>{ _t("Reason: %(reasonText)s", {reasonText: myMember.events.member.getContent().reason}) }</div>;
}
let rejoinBlock;
if (!banned) {
rejoinBlock = <div><a onClick={ this.props.onJoinClick }><b>{_t("Rejoin")}</b></a></div>;
rejoinBlock = <div><a onClick={this.props.onJoinClick}><b>{ _t("Rejoin") }</b></a></div>;
}
let actionText;
if (kicked) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
}
else if (banned) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
if (roomName) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
}
} else if (banned) {
if (roomName) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
}
} // no other options possible due to the kicked || banned check above.
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{actionText}
{ actionText }
<br />
{reason}
{rejoinBlock}
<a onClick={ this.props.onForgetClick }><b>{_t("Forget room")}</b></a>
{ reason }
{ rejoinBlock }
<a onClick={this.props.onForgetClick}><b>{ _t("Forget room") }</b></a>
</div>
</div>
);
} else if (this.props.error) {
var name = this.props.roomAlias || _t("This room");
var error;
const name = this.props.roomAlias || _t("This room");
let error;
if (this.props.error.errcode == 'M_NOT_FOUND') {
error = _t("%(roomName)s does not exist.", {roomName: name});
} else {
@ -205,12 +209,12 @@ module.exports = React.createClass({
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{ _t('You are trying to access %(roomName)s.', {roomName: name}) }
<br/>
{ _tJsx("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/,
(sub) => <a onClick={ this.props.onJoinClick }><b>{sub}</b></a>
)}
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
<br />
{ _t("<a>Click here</a> to join the discussion!",
{},
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
) }
</div>
</div>
);
@ -232,5 +236,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -17,28 +17,52 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import { _t, _tJsx } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SdkConfig from '../../../SdkConfig';
import sdk from '../../../index';
import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils';
import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val, def) {
var res = parseInt(val);
const res = parseInt(val);
return isNaN(res) ? def : res;
}
const plEventsToLabels = {
// These will be translated for us later.
"m.room.avatar": _td("To change the room's avatar, you must be a"),
"m.room.name": _td("To change the room's name, you must be a"),
"m.room.canonical_alias": _td("To change the room's main address, you must be a"),
"m.room.history_visibility": _td("To change the room's history visibility, you must be a"),
"m.room.power_levels": _td("To change the permissions in the room, you must be a"),
"m.room.topic": _td("To change the topic, you must be a"),
"im.vector.modular.widgets": _td("To modify widgets in the room, you must be a"),
};
const plEventsToShow = {
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
"m.room.avatar": {isState: true},
"m.room.name": {isState: true},
"m.room.canonical_alias": {isState: true},
"m.room.history_visibility": {isState: true},
"m.room.power_levels": {isState: true},
"m.room.topic": {isState: true},
"im.vector.modular.widgets": {isState: true},
};
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,
},
@ -47,6 +71,7 @@ const BannedUser = React.createClass({
Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, {
member: this.props.member,
action: _t('Unban'),
title: _t('Unban this user?'),
danger: false,
onFinished: (proceed) => {
if (!proceed) return;
@ -77,8 +102,10 @@ const BannedUser = React.createClass({
return (
<li>
{ unbanButton }
<strong>{this.props.member.name}</strong> {this.props.member.userId}
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
<span title={_t("Banned by %(displayName)s", {displayName: this.props.by})}>
<strong>{ this.props.member.name }</strong> { this.props.member.userId }
{ this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : "" }
</span>
</li>
);
},
@ -93,7 +120,7 @@ module.exports = React.createClass({
},
getInitialState: function() {
var tags = {};
const tags = {};
Object.keys(this.props.room.tags).forEach(function(tagName) {
tags[tagName] = ['yep'];
});
@ -122,7 +149,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership);
MatrixClientPeg.get().getRoomDirectoryVisibility(
this.props.room.roomId
this.props.room.roomId,
).done((result) => {
this.setState({ isRoomPublished: result.visibility === "public" });
this._originalIsRoomPublished = result.visibility === "public";
@ -131,9 +158,9 @@ module.exports = React.createClass({
});
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 0.3,
middleOpacity: 0.3,
action: 'panel_disable',
sideDisabled: true,
middleDisabled: true,
});
},
@ -144,21 +171,21 @@ module.exports = React.createClass({
}
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
action: 'panel_disable',
sideDisabled: false,
middleDisabled: false,
});
},
setName: function(name) {
this.setState({
name: name
name: name,
});
},
setTopic: function(topic) {
this.setState({
topic: topic
topic: topic,
});
},
@ -169,7 +196,7 @@ module.exports = React.createClass({
* `{ state: "fulfilled", value: v }` or `{ state: "rejected", reason: r }`.
*/
save: function() {
var stateWasSetDefer = Promise.defer();
const stateWasSetDefer = Promise.defer();
// the caller may have JUST called setState on stuff, so we need to re-render before saving
// else we won't use the latest values of things.
// We can be a bit cheeky here and set a loading flag, and listen for the callback on that
@ -196,8 +223,8 @@ module.exports = React.createClass({
_calcSavePromises: function() {
const roomId = this.props.room.roomId;
var promises = this.saveAliases(); // returns Promise[]
var originalState = this.getInitialState();
const promises = this.saveAliases(); // returns Promise[]
const originalState = this.getInitialState();
// diff between original state and this.state to work out what has been changed
console.log("Original: %s", JSON.stringify(originalState));
@ -215,14 +242,14 @@ module.exports = React.createClass({
promises.push(MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.history_visibility",
{ history_visibility: this.state.history_visibility },
""
"",
));
}
if (this.state.isRoomPublished !== originalState.isRoomPublished) {
promises.push(MatrixClientPeg.get().setRoomDirectoryVisibility(
roomId,
this.state.isRoomPublished ? "public" : "private"
this.state.isRoomPublished ? "public" : "private",
));
}
@ -230,7 +257,7 @@ module.exports = React.createClass({
promises.push(MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.join_rules",
{ join_rule: this.state.join_rule },
""
"",
));
}
@ -238,33 +265,33 @@ module.exports = React.createClass({
promises.push(MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.guest_access",
{ guest_access: this.state.guest_access },
""
"",
));
}
// power levels
var powerLevels = this._getPowerLevels();
const powerLevels = this._getPowerLevels();
if (powerLevels) {
promises.push(MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.power_levels", powerLevels, ""
roomId, "m.room.power_levels", powerLevels, "",
));
}
// tags
if (this.state.tags_changed) {
var tagDiffs = ObjectUtils.getKeyValueArrayDiffs(originalState.tags, this.state.tags);
const tagDiffs = ObjectUtils.getKeyValueArrayDiffs(originalState.tags, this.state.tags);
// [ {place: add, key: "m.favourite", val: ["yep"]} ]
tagDiffs.forEach(function(diff) {
switch (diff.place) {
case "add":
promises.push(
MatrixClientPeg.get().setRoomTag(roomId, diff.key, {})
MatrixClientPeg.get().setRoomTag(roomId, diff.key, {}),
);
break;
case "del":
promises.push(
MatrixClientPeg.get().deleteRoomTag(roomId, diff.key)
MatrixClientPeg.get().deleteRoomTag(roomId, diff.key),
);
break;
default:
@ -275,18 +302,21 @@ module.exports = React.createClass({
}
// color scheme
var p;
let p;
p = this.saveColor();
if (!p.isFulfilled()) {
promises.push(p);
}
// url preview settings
var ps = this.saveUrlPreviewSettings();
const ps = this.saveUrlPreviewSettings();
if (ps.length > 0) {
promises.push(ps);
ps.map((p) => promises.push(p));
}
// related groups
promises.push(this.saveRelatedGroups());
// encryption
p = this.saveEnableEncryption();
if (!p.isFulfilled()) {
@ -304,6 +334,11 @@ module.exports = React.createClass({
return this.refs.alias_settings.saveSettings();
},
saveRelatedGroups: function() {
if (!this.refs.related_groups) { return Promise.resolve(); }
return this.refs.related_groups.saveSettings();
},
saveColor: function() {
if (!this.refs.color_settings) { return Promise.resolve(); }
return this.refs.color_settings.saveSettings();
@ -317,37 +352,27 @@ module.exports = React.createClass({
saveEnableEncryption: function() {
if (!this.refs.encrypt) { return Promise.resolve(); }
var encrypt = this.refs.encrypt.checked;
const encrypt = this.refs.encrypt.checked;
if (!encrypt) { return Promise.resolve(); }
var roomId = this.props.room.roomId;
const roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.encryption",
{ algorithm: "m.megolm.v1.aes-sha2" }
{ algorithm: "m.megolm.v1.aes-sha2" },
);
},
saveBlacklistUnverifiedDevicesPerRoom: function() {
if (!this.refs.blacklistUnverified) return;
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
}
},
_isRoomBlacklistUnverified: function() {
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
if (blacklistUnverifiedDevicesPerRoom) {
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
}
return false;
},
_setRoomBlacklistUnverified: function(value) {
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
this.props.room.setBlacklistUnverifiedDevices(value);
if (!this.refs.blacklistUnverifiedDevices) return;
this.refs.blacklistUnverifiedDevices.save().then(() => {
const value = SettingsStore.getValueAt(
SettingLevel.ROOM_DEVICE,
"blacklistUnverifiedDevices",
this.props.room.roomId,
/*explicit=*/true,
);
this.props.room.setBlacklistUnverifiedDevices(value);
});
},
_hasDiff: function(strA, strB) {
@ -361,10 +386,15 @@ module.exports = React.createClass({
_getPowerLevels: function() {
if (!this.state.power_levels_changed) return undefined;
var powerLevels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
let powerLevels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
powerLevels = powerLevels ? powerLevels.getContent() : {};
var newPowerLevels = {
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());
}
const newPowerLevels = {
ban: parseInt(this.refs.ban.getValue()),
kick: parseInt(this.refs.kick.getValue()),
redact: parseInt(this.refs.redact.getValue()),
@ -381,39 +411,40 @@ module.exports = React.createClass({
onPowerLevelsChanged: function() {
this.setState({
power_levels_changed: true
power_levels_changed: true,
});
},
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
const event = this.props.room.currentState.getStateEvents(stateEventType, '');
if (!event) {
return defaultValue;
}
return event.getContent()[keyName] || defaultValue;
const content = event.getContent();
return keyName in content ? content[keyName] : defaultValue;
},
_onHistoryRadioToggle: function(ev) {
var self = this;
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const self = this;
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// cancel the click unless the user confirms it
ev.preventDefault();
var value = ev.target.value;
const value = ev.target.value;
Modal.createTrackedDialog('Privacy warning', '', QuestionDialog, {
title: _t('Privacy warning'),
description:
<div>
{ _t('Changes to who can read history will only apply to future messages in this room') }.<br/>
{ _t('Changes to who can read history will only apply to future messages in this room') }.<br />
{ _t('The visibility of existing history will be unchanged') }.
</div>,
button: _t('Continue'),
onFinished: function(confirmed) {
if (confirmed) {
self.setState({
history_visibility: value
history_visibility: value,
});
}
},
@ -421,7 +452,6 @@ module.exports = React.createClass({
},
_onRoomAccessRadioToggle: function(ev) {
// join_rule
// INVITE | PUBLIC
// ----------------------+----------------
@ -459,7 +489,7 @@ module.exports = React.createClass({
_onToggle: function(keyName, checkedValue, uncheckedValue, ev) {
console.log("Checkbox toggle: %s %s", keyName, ev.target.checked);
var state = {};
const state = {};
state[keyName] = ev.target.checked ? checkedValue : uncheckedValue;
this.setState(state);
},
@ -468,26 +498,24 @@ module.exports = React.createClass({
if (event.target.checked) {
if (tagName === 'm.favourite') {
delete this.state.tags['m.lowpriority'];
}
else if (tagName === 'm.lowpriority') {
} else if (tagName === 'm.lowpriority') {
delete this.state.tags['m.favourite'];
}
this.state.tags[tagName] = this.state.tags[tagName] || ["yep"];
}
else {
} else {
delete this.state.tags[tagName];
}
this.setState({
tags: this.state.tags,
tags_changed: true
tags_changed: true,
});
},
mayChangeRoomAccess: function() {
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) &&
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
},
@ -504,8 +532,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
var errCode = err.errcode || _t('unknown error code');
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const errCode = err.errcode || _t('unknown error code');
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t('Error'),
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
@ -516,7 +544,7 @@ module.exports = React.createClass({
onEnableEncryptionClick() {
if (!this.refs.encrypt.checked) return;
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('E2E Enable Warning', '', QuestionDialog, {
title: _t('Warning!'),
description: (
@ -528,7 +556,7 @@ module.exports = React.createClass({
<p>{ _t('Encrypted messages will not be visible on clients that do not yet implement encryption') }.</p>
</div>
),
onFinished: confirm=>{
onFinished: (confirm)=>{
if (!confirm) {
this.refs.encrypt.checked = false;
}
@ -541,26 +569,35 @@ module.exports = React.createClass({
this.forceUpdate();
},
_renderEncryptionSection: function() {
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
_populateDefaultPlEvents: function(eventsSection, stateLevel, eventsLevel) {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel);
}
}
},
var settings =
<label>
<input type="checkbox" ref="blacklistUnverified"
defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified }
disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/>
{ _t('Never send encrypted messages to unverified devices in this room from this device') }.
</label>;
_renderEncryptionSection: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
const settings = (
<SettingsFlag name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
roomId={this.props.room.roomId}
manualSave={true}
ref="blacklistUnverifiedDevices"
/>
);
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return (
<div>
<label>
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
<input type="checkbox" ref="encrypt" onClick={this.onEnableEncryptionClick} />
<img className="mx_RoomSettings_e2eIcon mx_filterFlipColor" src="img/e2e-unencrypted.svg" width="12" height="12" />
{ _t('Enable encryption') } { _t('(warning: cannot be disabled again!)') }
</label>
@ -587,58 +624,64 @@ module.exports = React.createClass({
// TODO: go through greying out things you don't have permission to change
// (or turning them into informative stuff)
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var Loader = sdk.getComponent("elements.Spinner");
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
const PowerSelector = sdk.getComponent('elements.PowerSelector');
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var user_id = cli.credentials.userId;
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const user_id = cli.credentials.userId;
var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
var power_levels = power_level_event ? power_level_event.getContent() : {};
var events_levels = power_levels.events || {};
var user_levels = power_levels.users || {};
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 || {};
var ban_level = parseIntWithDefault(power_levels.ban, 50);
var kick_level = parseIntWithDefault(power_levels.kick, 50);
var redact_level = parseIntWithDefault(power_levels.redact, 50);
var invite_level = parseIntWithDefault(power_levels.invite, 50);
var send_level = parseIntWithDefault(power_levels.events_default, 0);
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
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);
var current_user_level = user_levels[user_id];
this._populateDefaultPlEvents(events_levels, state_level, send_level);
let current_user_level = user_levels[user_id];
if (current_user_level === undefined) {
current_user_level = default_user_level;
}
var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
const can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
var canSetTag = !cli.isGuest();
const canSetTag = !cli.isGuest();
var self = this;
const self = this;
var userLevelsSection;
const relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
roomId={this.props.room.roomId}
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')}
/>;
let userLevelsSection;
if (Object.keys(user_levels).length) {
userLevelsSection =
<div>
<h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{Object.keys(user_levels).map(function(user, i) {
{ Object.keys(user_levels).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", {user: user}) } <PowerSelector value={user_levels[user]} disabled={true} />
</li>
);
})}
}) }
</ul>
</div>;
}
else {
} else {
userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
}
@ -650,18 +693,21 @@ module.exports = React.createClass({
<div>
<h3>{ _t('Banned users') }</h3>
<ul className="mx_RoomSettings_banned">
{banned.map(function(member) {
{ banned.map(function(member) {
const banEvent = member.events.member.getContent();
const sender = self.props.room.getMember(member.events.member.getSender());
let bannedBy = member.events.member.getSender(); // start by falling back to mxid
if (sender) bannedBy = sender.name;
return (
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} by={bannedBy} />
);
})}
}) }
</ul>
</div>;
}
var unfederatableSection;
if (this._yankValueFromEvent("m.room.create", "m.federate") === false) {
let unfederatableSection;
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') }.
@ -669,19 +715,18 @@ module.exports = React.createClass({
);
}
var leaveButton = null;
var myMember = this.props.room.getMember(user_id);
let leaveButton = null;
const myMember = this.props.room.getMember(user_id);
if (myMember) {
if (myMember.membership === "join") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onLeaveClick}>
{ _t('Leave room') }
</AccessibleButton>
);
}
else if (myMember.membership === "leave") {
} else if (myMember.membership === "leave") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onForgetClick}>
{ _t('Forget room') }
</AccessibleButton>
);
@ -691,7 +736,7 @@ module.exports = React.createClass({
// TODO: support editing custom events_levels
// TODO: support editing custom user_levels
var tags = [
const tags = [
{ name: "m.favourite", label: _t('Favourite'), ref: "tag_favourite" },
{ name: "m.lowpriority", label: _t('Low priority'), ref: "tag_lowpriority" },
];
@ -702,17 +747,17 @@ module.exports = React.createClass({
}
});
var tagsSection = null;
let tagsSection = null;
if (canSetTag || self.state.tags) {
var tagsSection =
tagsSection =
<div className="mx_RoomSettings_tags">
{_t("Tagged as: ")}{ canSetTag ?
{ _t("Tagged as: ") }{ canSetTag ?
(tags.map(function(tag, i) {
return (<label key={ i }>
return (<label key={i}>
<input type="checkbox"
ref={ tag.ref }
checked={ tag.name in self.state.tags }
onChange={ self._onTagChange.bind(self, tag.name) }/>
ref={tag.ref}
checked={tag.name in self.state.tags}
onChange={self._onTagChange.bind(self, tag.name)} />
{ tag.label }
</label>);
})) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : ""
@ -722,11 +767,11 @@ module.exports = React.createClass({
// If there is no history_visibility, it is assumed to be 'shared'.
// http://matrix.org/docs/spec/r0.0.0/client_server.html#id31
var historyVisibility = this.state.history_visibility || "shared";
const historyVisibility = this.state.history_visibility || "shared";
var addressWarning;
var aliasEvents = this.props.room.currentState.getStateEvents('m.room.aliases') || [];
var aliasCount = 0;
let addressWarning;
const aliasEvents = this.props.room.currentState.getStateEvents('m.room.aliases') || [];
let aliasCount = 0;
aliasEvents.forEach((event) => {
aliasCount += event.getContent().aliases.length;
});
@ -734,19 +779,19 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning =
<div className="mx_RoomSettings_warning">
{ _tJsx(
{ _t(
'To link to a room it must have <a>an address</a>.',
/<a>(.*?)<\/a>/,
(sub) => <a href="#addresses">{sub}</a>
)}
{},
{ 'a': (sub) => <a href="#addresses">{ sub }</a> },
) }
</div>;
}
var inviteGuestWarning;
let inviteGuestWarning;
if (this.state.join_rule !== "public" && this.state.guest_access === "forbidden") {
inviteGuestWarning =
<div className="mx_RoomSettings_warning">
{ _t('Guests cannot join this room even if explicitly invited.') } <a href="#" onClick={ (e) => {
{ _t('Guests cannot join this room even if explicitly invited.') } <a href="#" onClick={(e) => {
this.setState({ join_rule: "invite", guest_access: "can_join" });
e.preventDefault();
}}>{ _t('Click here to fix') }</a>.
@ -766,64 +811,64 @@ module.exports = React.createClass({
{ inviteGuestWarning }
<label>
<input type="radio" name="roomVis" value="invite_only"
disabled={ !this.mayChangeRoomAccess() }
disabled={!this.mayChangeRoomAccess()}
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule !== "public"}/>
checked={this.state.join_rule !== "public"} />
{ _t('Only people who have been invited') }
</label>
<label>
<input type="radio" name="roomVis" value="public_no_guests"
disabled={ !this.mayChangeRoomAccess() }
disabled={!this.mayChangeRoomAccess()}
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"}/>
checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"} />
{ _t('Anyone who knows the room\'s link, apart from guests') }
</label>
<label>
<input type="radio" name="roomVis" value="public_with_guests"
disabled={ !this.mayChangeRoomAccess() }
disabled={!this.mayChangeRoomAccess()}
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"}/>
checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"} />
{ _t('Anyone who knows the room\'s link, including guests') }
</label>
{ addressWarning }
<br/>
<br />
{ this._renderEncryptionSection() }
<label>
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
checked={this.state.isRoomPublished}/>
{_t("Publish this room to the public in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
<input type="checkbox" disabled={!roomState.mayClientSendStateEvent("m.room.aliases", cli)}
onChange={this._onToggle.bind(this, "isRoomPublished", true, false)}
checked={this.state.isRoomPublished} />
{ _t("Publish this room to the public in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() }) }
</label>
</div>
<div className="mx_RoomSettings_settings">
<h3>{ _t('Who can read history?') }</h3>
<label>
<input type="radio" name="historyVis" value="world_readable"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "world_readable"}
onChange={this._onHistoryRadioToggle} />
{_t("Anyone")}
{ _t("Anyone") }
</label>
<label>
<input type="radio" name="historyVis" value="shared"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "shared"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since the point in time of selecting this option') })
{ _t('Members only (since the point in time of selecting this option)') }
</label>
<label>
<input type="radio" name="historyVis" value="invited"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "invited"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since they were invited') })
{ _t('Members only (since they were invited)') }
</label>
<label >
<input type="radio" name="historyVis" value="joined"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "joined"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since they joined') })
{ _t('Members only (since they joined)') }
</label>
</div>
</div>
@ -834,11 +879,11 @@ module.exports = React.createClass({
<ColorSettings ref="color_settings" room={this.props.room} />
</div>
<a id="addresses"/>
<a id="addresses" />
<AliasSettings ref="alias_settings"
roomId={this.props.room.roomId}
canSetCanonicalAlias={ roomState.mayClientSendStateEvent("m.room.canonical_alias", cli) }
canSetCanonicalAlias={roomState.mayClientSendStateEvent("m.room.canonical_alias", cli)}
canSetAliases={
true
/* Originally, we arbitrarily restricted creating aliases to room admins: roomState.mayClientSendStateEvent("m.room.aliases", cli) */
@ -846,47 +891,53 @@ module.exports = React.createClass({
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
{ relatedGroupsSection }
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<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} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
<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') }, { _t('you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
<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) {
{ 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">{ _t('To send events of type') } <code>{ event_type }</code>, { _t('you must be a') } </span>
<PowerSelector value={ events_levels[event_type] } controlled={false} disabled={true} onChange={self.onPowerLevelsChanged}/>
<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 }
</div>
@ -901,5 +952,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

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.
@ -16,17 +17,18 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require("react-dom");
var classNames = require('classnames');
var MatrixClientPeg = require('../../../MatrixClientPeg');
const React = require('react');
const ReactDOM = require("react-dom");
const classNames = require('classnames');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import DMRoomMap from '../../../utils/DMRoomMap';
var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils');
const sdk = require('../../../index');
const ContextualMenu = require('../../structures/ContextualMenu');
const RoomNotifs = require('../../../RoomNotifs');
const FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore');
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = React.createClass({
displayName: 'RoomTile',
@ -39,7 +41,6 @@ module.exports = React.createClass({
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
@ -53,11 +54,12 @@ module.exports = React.createClass({
},
getInitialState: function() {
return({
hover : false,
badgeHover : false,
return ({
hover: false,
badgeHover: false,
menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
@ -71,7 +73,7 @@ module.exports = React.createClass({
},
_isDirectMessageRoom: function(roomId) {
var dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (dmRooms) {
return true;
} else {
@ -87,15 +89,23 @@ module.exports = React.createClass({
}
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
},
componentWillUnmount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
},
onClick: function(ev) {
@ -105,12 +115,12 @@ module.exports = React.createClass({
},
onMouseEnter: function() {
this.setState( { hover : true });
this.setState( { hover: true });
this.badgeOnMouseEnter();
},
onMouseLeave: function() {
this.setState( { hover : false });
this.setState( { hover: false });
this.badgeOnMouseLeave();
},
@ -118,25 +128,24 @@ module.exports = React.createClass({
// Only allow non-guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
this.setState( { badgeHover: true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } );
this.setState( { badgeHover: false } );
},
onBadgeClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest()) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
var elementRect = e.target.getBoundingClientRect();
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
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;
@ -144,7 +153,7 @@ module.exports = React.createClass({
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
var self = this;
const self = this;
ContextualMenu.createMenu(RoomTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
@ -153,7 +162,7 @@ module.exports = React.createClass({
onFinished: function() {
self.setState({ menuDisplayed: false });
self.props.refreshSubList();
}
},
});
this.setState({ menuDisplayed: true });
}
@ -162,19 +171,19 @@ module.exports = React.createClass({
},
render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
const myUserId = MatrixClientPeg.get().credentials.userId;
const me = this.props.room.currentState.members[myUserId];
var notificationCount = this.props.room.getUnreadNotificationCount();
const notificationCount = this.props.room.getUnreadNotificationCount();
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
@ -183,53 +192,53 @@ module.exports = React.createClass({
'mx_RoomTile_noBadges': !badges,
});
var avatarClasses = classNames({
const avatarClasses = classNames({
'mx_RoomTile_avatar': true,
});
var badgeClasses = classNames({
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'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)
var name = this.props.room.name || this.props.room.roomId;
let name = this.props.room.name || this.props.room.roomId;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge;
var badgeContent;
let badge;
let badgeContent;
if (this.state.badgeHover || this.state.menuDisplayed) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (badges) {
var limitedCount = FormattingUtils.formatCount(notificationCount);
const limitedCount = FormattingUtils.formatCount(notificationCount);
badgeContent = notificationCount ? limitedCount : '!';
} else {
badgeContent = '\u200B';
}
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked}>{ badgeContent }</div>;
badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
const EmojiText = sdk.getComponent('elements.EmojiText');
var label;
var tooltip;
let label;
let tooltip;
if (!this.props.collapsed) {
var nameClasses = classNames({
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
if (this.props.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
} else {
label = <EmojiText element="div" title={ name } className={ nameClasses } dir="auto">{name}</EmojiText>;
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
}
} else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
}
@ -239,34 +248,34 @@ module.exports = React.createClass({
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
//}
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var directMessageIndicator;
let directMessageIndicator;
if (this._isDirectMessageRoom(this.props.room.roomId)) {
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm"/>;
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:
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
var connectDropTarget = this.props.connectDropTarget;
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. */}
<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}
{ directMessageIndicator }
</div>
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{/* { incomingCallBox } */}
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
</div>
@ -276,5 +285,5 @@ module.exports = React.createClass({
if (connectDragSource) ret = connectDragSource(ret);
return ret;
}
},
});

View file

@ -16,8 +16,8 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
const React = require('react');
const sdk = require('../../../index');
import { _t } from "../../../languageHandler";
module.exports = React.createClass({
@ -28,8 +28,8 @@ module.exports = React.createClass({
},
componentWillMount: function() {
var room = this.props.room;
var topic = room.currentState.getStateEvents('m.room.topic', '');
const room = this.props.room;
const topic = room.currentState.getStateEvents('m.room.topic', '');
this._initialTopic = topic ? topic.getContent().topic : '';
},
@ -38,15 +38,15 @@ module.exports = React.createClass({
},
render: function() {
var EditableText = sdk.getComponent("elements.EditableText");
const EditableText = sdk.getComponent("elements.EditableText");
return (
<EditableText ref="editor"
className="mx_RoomHeader_topic mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={_t("Add a topic")}
blurToCancel={ false }
initialValue={ this._initialTopic }
blurToCancel={false}
initialValue={this._initialTopic}
dir="auto" />
);
},

View file

@ -16,8 +16,8 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
const React = require('react');
const sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'SearchResult',
@ -36,20 +36,20 @@ module.exports = React.createClass({
},
render: function() {
var DateSeparator = sdk.getComponent('messages.DateSeparator');
var EventTile = sdk.getComponent('rooms.EventTile');
var result = this.props.searchResult;
var mxEv = result.context.getEvent();
var eventId = mxEv.getId();
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventTile = sdk.getComponent('rooms.EventTile');
const result = this.props.searchResult;
const mxEv = result.context.getEvent();
const eventId = mxEv.getId();
var ts1 = mxEv.getTs();
var ret = [<DateSeparator key={ts1 + "-search"} ts={ts1}/>];
const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
var timeline = result.context.getTimeline();
const timeline = result.context.getTimeline();
for (var j = 0; j < timeline.length; j++) {
var ev = timeline[j];
const ev = timeline[j];
var highlights;
var contextual = (j != result.context.getOurEventIndex());
const contextual = (j != result.context.getOurEventIndex());
if (!contextual) {
highlights = this.props.searchHighlights;
}
@ -61,7 +61,7 @@ module.exports = React.createClass({
}
return (
<li data-scroll-tokens={eventId+"+"+j}>
{ret}
{ ret }
</li>);
},
});

View file

@ -13,16 +13,16 @@ 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.
*/
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require("../../../index");
const React = require('react');
const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal");
const sdk = require("../../../index");
import { _t } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
const GeminiScrollbar = require('react-gemini-scrollbar');
// A list capable of displaying entities which conform to the SearchableEntity
// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
var SearchableEntityList = React.createClass({
const SearchableEntityList = React.createClass({
displayName: 'SearchableEntityList',
propTypes: {
@ -31,7 +31,7 @@ var SearchableEntityList = React.createClass({
onQueryChanged: React.PropTypes.func, // fn(inputText)
onSubmit: React.PropTypes.func, // fn(inputText)
entities: React.PropTypes.array,
truncateAt: React.PropTypes.number
truncateAt: React.PropTypes.number,
},
getDefaultProps: function() {
@ -40,7 +40,7 @@ var SearchableEntityList = React.createClass({
entities: [],
emptyQueryShowsAll: false,
onSubmit: function() {},
onQueryChanged: function(input) {}
onQueryChanged: function(input) {},
};
},
@ -49,14 +49,14 @@ var SearchableEntityList = React.createClass({
query: "",
focused: false,
truncateAt: this.props.truncateAt,
results: this.getSearchResults("", this.props.entities)
results: this.getSearchResults("", this.props.entities),
};
},
componentWillReceiveProps: function(newProps) {
// recalculate the search results in case we got new entities
this.setState({
results: this.getSearchResults(this.state.query, newProps.entities)
results: this.getSearchResults(this.state.query, newProps.entities),
});
},
@ -73,17 +73,17 @@ var SearchableEntityList = React.createClass({
setQuery: function(input) {
this.setState({
query: input,
results: this.getSearchResults(input, this.props.entities)
results: this.getSearchResults(input, this.props.entities),
});
},
onQueryChanged: function(ev) {
var q = ev.target.value;
const q = ev.target.value;
this.setState({
query: q,
// reset truncation if they back out the entire text
truncateAt: (q.length === 0 ? this.props.truncateAt : this.state.truncateAt),
results: this.getSearchResults(q, this.props.entities)
results: this.getSearchResults(q, this.props.entities),
}, () => {
// invoke the callback AFTER we've flushed the new state. We need to
// do this because onQueryChanged can result in new props being passed
@ -110,13 +110,13 @@ var SearchableEntityList = React.createClass({
_showAll: function() {
this.setState({
truncateAt: -1
truncateAt: -1,
});
},
_createOverflowEntity: function(overflowCount, totalCount) {
var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
@ -127,7 +127,7 @@ var SearchableEntityList = React.createClass({
},
render: function() {
var inputBox;
let inputBox;
if (this.props.showInputBox) {
inputBox = (
@ -136,31 +136,30 @@ var SearchableEntityList = React.createClass({
onChange={this.onQueryChanged} value={this.state.query}
onFocus= {() => { this.setState({ focused: true }); }}
onBlur= {() => { this.setState({ focused: false }); }}
placeholder={ _t("Search") } />
placeholder={_t("Search")} />
</form>
);
}
var list;
let list;
if (this.state.results.length > 1 || this.state.focused) {
if (this.props.truncateAt) { // caller wants list truncated
var TruncatedList = sdk.getComponent("elements.TruncatedList");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
list = (
<TruncatedList className="mx_SearchableEntityList_list"
truncateAt={this.state.truncateAt} // use state truncation as it may be expanded
createOverflowElement={this._createOverflowEntity}>
{this.state.results.map((entity) => {
{ this.state.results.map((entity) => {
return entity.getJsx();
})}
}) }
</TruncatedList>
);
}
else {
} else {
list = (
<div className="mx_SearchableEntityList_list">
{this.state.results.map((entity) => {
{ this.state.results.map((entity) => {
return entity.getJsx();
})}
}) }
</div>
);
}
@ -173,13 +172,13 @@ var SearchableEntityList = React.createClass({
}
return (
<div className={ "mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "") }>
<div className={"mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "")}>
{ inputBox }
{ list }
{ list ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
{ list ? <div className="mx_SearchableEntityList_hrWrapper"><hr /></div> : '' }
</div>
);
}
},
});
module.exports = SearchableEntityList;

View file

@ -26,7 +26,7 @@ export function CancelButton(props) {
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/>
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}

View file

@ -17,9 +17,9 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
const sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'TopUnreadMessagesBar',
@ -35,8 +35,8 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}>
<img src="img/scrollto.svg" width="24" height="24"
alt={ _t('Scroll to unread messages') }
title={ _t('Scroll to unread messages') }/>
alt={_t('Scroll to unread messages')}
title={_t('Scroll to unread messages')} />
{ _t("Jump to first unread message.") }
</div>
<img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"

View file

@ -16,41 +16,41 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var Avatar = require("../../../Avatar");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
const Avatar = require("../../../Avatar");
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
const dis = require('../../../dispatcher');
const Modal = require("../../../Modal");
module.exports = React.createClass({
displayName: 'UserTile',
propTypes: {
user: React.PropTypes.any.isRequired // User
user: React.PropTypes.any.isRequired, // User
},
render: function() {
var EntityTile = sdk.getComponent("rooms.EntityTile");
var user = this.props.user;
var name = user.displayName || user.userId;
var active = -1;
const EntityTile = sdk.getComponent("rooms.EntityTile");
const user = this.props.user;
const name = user.displayName || user.userId;
let active = -1;
// FIXME: make presence data update whenever User.presence changes...
active = user.lastActiveAgo ?
(Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1;
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
var avatarJsx = (
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatarJsx = (
<BaseAvatar width={36} height={36} name={name} idName={user.userId}
url={ Avatar.avatarUrlForUser(user, 36, 36, "crop") } />
url={Avatar.avatarUrlForUser(user, 36, 36, "crop")} />
);
return (
<EntityTile {...this.props} presenceState={user.presence} presenceActiveAgo={active}
presenceCurrentlyActive={ user.currentlyActive }
presenceCurrentlyActive={user.currentlyActive}
name={name} title={user.userId} avatarJsx={avatarJsx} />
);
}
},
});