Merge branch 'develop' into t3chguy/hide_left_chats_memberinfo
This commit is contained in:
commit
831a3f7e42
311 changed files with 28762 additions and 12120 deletions
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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}) }
|
||||
|
||||
{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}} >
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
90
src/components/views/rooms/PinnedEventTile.js
Normal file
90
src/components/views/rooms/PinnedEventTile.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
127
src/components/views/rooms/PinnedEventsPanel.js
Normal file
127
src/components/views/rooms/PinnedEventsPanel.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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)},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
148
src/components/views/rooms/RoomDetailList.js
Normal file
148
src/components/views/rooms/RoomDetailList.js
Normal 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>
|
||||
{ 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>;
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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" />
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue