Merge remote-tracking branch 'origin/develop' into fix_riot_web_4821
This commit is contained in:
commit
36df3acf4d
137 changed files with 5949 additions and 2020 deletions
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import * as sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Presence from "../../../Presence";
|
||||
import dispatcher from "../../../dispatcher";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// This is an avatar with presence information and controls on it.
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberPresenceAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
let presenceState = null;
|
||||
let presenceMessage = null;
|
||||
|
||||
// RoomMembers do not necessarily have a user.
|
||||
if (this.props.member.user) {
|
||||
presenceState = this.props.member.user.presence;
|
||||
presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
}
|
||||
|
||||
return {
|
||||
status: presenceState,
|
||||
message: presenceMessage,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
|
||||
this.dispatcherRef = dispatcher.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
|
||||
}
|
||||
dispatcher.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "self_presence_updated") return;
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: payload.statusInfo.presence,
|
||||
message: payload.statusInfo.status_msg,
|
||||
});
|
||||
},
|
||||
|
||||
onUserPresence: function(event, user) {
|
||||
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: user.presence,
|
||||
message: user.presenceStatusMsg,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusChange: function(newStatus) {
|
||||
Presence.stopMaintainingStatus();
|
||||
if (newStatus === "online") {
|
||||
Presence.setState(newStatus);
|
||||
} else Presence.setState(newStatus, null, true);
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
|
||||
|
||||
ContextualMenu.createMenu(PresenceContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: 125,
|
||||
currentStatus: this.state.status,
|
||||
onChange: this.onStatusChange,
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
// XXX NB the following assumes that user is non-null, which is not valid
|
||||
// const presenceState = this.props.member.user.presence;
|
||||
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
// const presenceLastTs = this.props.member.user.lastPresenceTs;
|
||||
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
// const presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
|
||||
|
||||
let onClickFn = null;
|
||||
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
|
||||
onClickFn = this.onClick;
|
||||
}
|
||||
|
||||
const avatarNode = (
|
||||
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod} />
|
||||
);
|
||||
let statusNode = (
|
||||
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
|
||||
);
|
||||
|
||||
// LABS: Disable presence management functions for now
|
||||
// Also disable the presence information if there's no status information
|
||||
if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
|
||||
statusNode = null;
|
||||
onClickFn = null;
|
||||
}
|
||||
|
||||
let avatar = (
|
||||
<div className="mx_MemberPresenceAvatar">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</div>
|
||||
);
|
||||
if (onClickFn) {
|
||||
avatar = (
|
||||
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
return avatar;
|
||||
},
|
||||
});
|
|
@ -244,7 +244,7 @@ module.exports = React.createClass({
|
|||
|
||||
_doNaiveGroupRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
|
||||
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
const results = [];
|
||||
groupStore.getGroupRooms().forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
|
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
danger: false,
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = _t("Confirm Removal");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': false,
|
||||
});
|
||||
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{ _t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.") }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
{ _t("Remove") }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ export default React.createClass({
|
|||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if(!deviceInfo) {
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
|
@ -45,9 +45,10 @@ export default React.createClass({
|
|||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>,
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
|
|
|
@ -20,8 +20,8 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
|
@ -267,24 +267,21 @@ export default React.createClass({
|
|||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -15,11 +16,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function markAllDevicesKnown(devices) {
|
||||
Object.keys(devices).forEach((userId) => {
|
||||
Object.keys(devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -37,10 +48,10 @@ function DeviceListEntry(props) {
|
|||
}
|
||||
|
||||
DeviceListEntry.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// deviceinfo
|
||||
device: React.PropTypes.object.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -60,10 +71,10 @@ function UserUnknownDeviceList(props) {
|
|||
}
|
||||
|
||||
UserUnknownDeviceList.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// map from deviceid -> deviceinfo
|
||||
userDevices: React.PropTypes.object.isRequired,
|
||||
userDevices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -82,7 +93,7 @@ function UnknownDeviceList(props) {
|
|||
|
||||
UnknownDeviceList.propTypes = {
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
devices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -90,34 +101,65 @@ export default React.createClass({
|
|||
displayName: 'UnknownDeviceDialog',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
|
||||
devices: PropTypes.object,
|
||||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Label for the button that marks all devices known and tries the send again
|
||||
sendAnywayLabel: PropTypes.string.isRequired,
|
||||
|
||||
// Label for the button that to send the event if you've verified all devices
|
||||
sendLabel: PropTypes.string.isRequired,
|
||||
|
||||
// function to retry the request once all devices are verified / known
|
||||
onSend: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Given we've now shown the user the unknown device, it is no longer
|
||||
// unknown to them. Therefore mark it as 'known'.
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Opening UnknownDeviceDialog');
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
|
||||
// XXX: Mutating props :/
|
||||
this.props.devices[userId][deviceId] = deviceInfo;
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
_onDismissClicked: function() {
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
_onSendAnywayClicked: function() {
|
||||
markAllDevicesKnown(this.props.devices);
|
||||
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
_onSendClicked: function() {
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||
this.props.room.getBlacklistUnverifiedDevices();
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (blacklistUnverified) {
|
||||
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||
warning = (
|
||||
<h4>
|
||||
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||
|
@ -136,15 +178,30 @@ export default React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let haveUnknownDevices = false;
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
const device = this.props.devices[userId][deviceId];
|
||||
if (device.isUnverified() && !device.isKnown()) {
|
||||
haveUnknownDevices = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
let sendButton;
|
||||
if (haveUnknownDevices) {
|
||||
sendButton = <button onClick={this._onSendAnywayClicked}>
|
||||
{ this.props.sendAnywayLabel }
|
||||
</button>;
|
||||
} else {
|
||||
sendButton = <button onClick={this._onSendClicked}>
|
||||
{ this.props.sendLabel }
|
||||
</button>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by escape");
|
||||
this.props.onFinished();
|
||||
}}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Room contains unknown devices')}
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
|
@ -157,21 +214,11 @@ export default React.createClass({
|
|||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{sendButton}
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={() => {
|
||||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
{ _t("Send anyway") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by OK");
|
||||
this.props.onFinished();
|
||||
}}>
|
||||
OK
|
||||
onClick={this._onDismissClicked}
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -77,6 +77,7 @@ export default React.createClass({
|
|||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||
{ tooltip }
|
||||
|
|
|
@ -19,9 +19,9 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if(curl) {
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
curlString = curl.format();
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default class AppPermission extends React.Component {
|
|||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if(wurl && wurl.hostname && (
|
||||
if (wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
|
|
|
@ -22,6 +22,8 @@ import React from 'react';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
@ -50,11 +52,13 @@ export default React.createClass({
|
|||
userId: React.PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: React.PropTypes.string,
|
||||
waitForIframeLoad: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -69,17 +73,46 @@ export default React.createClass({
|
|||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: true, // True while the iframe content is loading
|
||||
widgetUrl: newProps.url,
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
const u = url.parse(urlString);
|
||||
if (!u) {
|
||||
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||
return url;
|
||||
}
|
||||
|
||||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL
|
||||
params.parentUrl = window.location.href;
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this._getNewState(this.props);
|
||||
},
|
||||
|
@ -121,6 +154,8 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount() {
|
||||
WidgetMessaging.startListening();
|
||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
this.setScalarToken();
|
||||
},
|
||||
|
@ -136,7 +171,7 @@ export default React.createClass({
|
|||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this.props.url,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
|
@ -149,7 +184,7 @@ export default React.createClass({
|
|||
this._scalarClient.getScalarToken().done((token) => {
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this.props.url);
|
||||
const u = url.parse(this._addWurlParams(this.props.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
|
@ -163,6 +198,11 @@ export default React.createClass({
|
|||
widgetUrl: u.format(),
|
||||
initialising: false,
|
||||
});
|
||||
|
||||
// Fetch page title from remote content if not already set
|
||||
if (!this.state.widgetPageTitle && params.url) {
|
||||
this._fetchWidgetTitle(params.url);
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to get scalar_token", err);
|
||||
this.setState({
|
||||
|
@ -173,6 +213,8 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetMessaging.stopListening();
|
||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
},
|
||||
|
||||
|
@ -180,10 +222,14 @@ export default React.createClass({
|
|||
if (nextProps.url !== this.props.url) {
|
||||
this._getNewState(nextProps);
|
||||
this.setScalarToken();
|
||||
} else if (nextProps.show && !this.props.show) {
|
||||
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||
this.setState({
|
||||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -255,10 +301,27 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
this.setState({loading: false});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_fetchWidgetTitle(url) {
|
||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||
if (widgetPageTitle) {
|
||||
this.setState({widgetPageTitle: widgetPageTitle});
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
},
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
_deleteWidgetLabel() {
|
||||
|
@ -283,7 +346,7 @@ export default React.createClass({
|
|||
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
if (this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
|
@ -304,6 +367,15 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
},
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -319,11 +391,6 @@ export default React.createClass({
|
|||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
|
@ -346,7 +413,7 @@ export default React.createClass({
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
ref="appFrame"
|
||||
src={safeWidgetUrl}
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded}
|
||||
|
@ -371,35 +438,50 @@ export default React.createClass({
|
|||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||
let deleteIcon = 'img/cancel.svg';
|
||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||
if(this._canUserModify()) {
|
||||
let deleteIcon = 'img/cancel_green.svg';
|
||||
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||
if (this._canUserModify()) {
|
||||
deleteIcon = 'img/icon-delete-pink.svg';
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
{ this.formatAppTileName() }
|
||||
<span className="mx_AppTileMenuBarTitle">
|
||||
<TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||
<span> - { this.state.widgetPageTitle }</span>
|
||||
) }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
<img src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
alt={_t(deleteWidgetLabel)}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
<TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
85
src/components/views/elements/DNDTagTile.js
Normal file
85
src/components/views/elements/DNDTagTile.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* eslint new-cap: "off" */
|
||||
/*
|
||||
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 { DragSource, DropTarget } from 'react-dnd';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import dis from '../../../dispatcher';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
const tagTileSource = {
|
||||
canDrag: function(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
beginDrag: function(props) {
|
||||
// Return the data describing the dragged item
|
||||
return {
|
||||
tag: props.tag,
|
||||
};
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor, component) {
|
||||
const dropResult = monitor.getDropResult();
|
||||
if (!monitor.didDrop() || !dropResult) {
|
||||
return;
|
||||
}
|
||||
props.onEndDrag();
|
||||
},
|
||||
};
|
||||
|
||||
const tagTileTarget = {
|
||||
canDrop(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
hover(props, monitor, component) {
|
||||
if (!monitor.canDrop()) return;
|
||||
const draggedY = monitor.getClientOffset().y;
|
||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||
const targetY = (top + bottom) / 2;
|
||||
dis.dispatch({
|
||||
action: 'order_tag',
|
||||
tag: monitor.getItem().tag,
|
||||
targetTag: props.tag,
|
||||
// Note: we indicate that the tag should be after the target when
|
||||
// it's being dragged over the top half of the target.
|
||||
after: draggedY < targetY,
|
||||
});
|
||||
},
|
||||
|
||||
drop(props) {
|
||||
// Return the data to be returned by getDropResult
|
||||
return {
|
||||
tag: props.tag,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default
|
||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
}))((props) => {
|
||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||
return connectDropTarget(connectDragSource(
|
||||
<div>
|
||||
<TagTile {...otherProps} />
|
||||
</div>,
|
||||
));
|
||||
}));
|
|
@ -72,24 +72,19 @@ export default class Flair extends React.Component {
|
|||
this.state = {
|
||||
profiles: [],
|
||||
};
|
||||
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._generateAvatars();
|
||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||
this._generateAvatars(this.props.groups);
|
||||
}
|
||||
|
||||
onRoomStateEvents(event) {
|
||||
if (event.getType() === 'm.room.related_groups' && FlairStore.groupSupport()) {
|
||||
this._generateAvatars();
|
||||
}
|
||||
componentWillReceiveProps(newProps) {
|
||||
this._generateAvatars(newProps.groups);
|
||||
}
|
||||
|
||||
async _getGroupProfiles(groups) {
|
||||
|
@ -106,23 +101,7 @@ export default class Flair extends React.Component {
|
|||
return profiles.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
async _generateAvatars() {
|
||||
let groups = await FlairStore.getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
|
||||
if (this.props.roomId && this.props.showRelated) {
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.roomId)
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
const relatedGroups = relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || [] : [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
groups = groups.filter((groupId) => {
|
||||
return relatedGroups.includes(groupId);
|
||||
});
|
||||
} else {
|
||||
groups = [];
|
||||
}
|
||||
}
|
||||
async _generateAvatars(groups) {
|
||||
if (!groups || groups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -148,13 +127,7 @@ export default class Flair extends React.Component {
|
|||
}
|
||||
|
||||
Flair.propTypes = {
|
||||
userId: PropTypes.string,
|
||||
|
||||
// Whether to show only the flair associated with related groups of the given room,
|
||||
// or all flair associated with a user.
|
||||
showRelated: PropTypes.bool,
|
||||
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
|
||||
roomId: PropTypes.string,
|
||||
groups: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
|
@ -41,8 +41,8 @@ export default class LanguageDropdown extends React.Component {
|
|||
componentWillMount() {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b) {
|
||||
if(a.label < b.label) return -1;
|
||||
if(a.label > b.label) return 1;
|
||||
if (a.label < b.label) return -1;
|
||||
if (a.label > b.label) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.setState({langs});
|
||||
|
@ -54,10 +54,10 @@ export default class LanguageDropdown extends React.Component {
|
|||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
this.props.onOptionChange(_localSettings.language);
|
||||
}else {
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
} else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
|
@ -95,12 +95,12 @@ export default class LanguageDropdown extends React.Component {
|
|||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
value = this.props.value || _localSettings.language;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
const language = navigator.language || navigator.userLanguage;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
|
|
|
@ -216,7 +216,7 @@ module.exports = React.createClass({
|
|||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
let res = null;
|
||||
switch(t) {
|
||||
switch (t) {
|
||||
case "joined":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
|
||||
|
@ -304,7 +304,7 @@ module.exports = React.createClass({
|
|||
return items[0];
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } )
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
|
@ -478,7 +478,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
const toggleButton = (
|
||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||
{ expanded ? 'collapse' : 'expand' }
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -20,14 +20,16 @@ import React from 'react';
|
|||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
let LEVEL_ROLE_MAP = {};
|
||||
const reverseRoles = {};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: React.PropTypes.number.isRequired,
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: React.PropTypes.number.isRequired,
|
||||
|
||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||
// to reflect the current value, rather than left freeform.
|
||||
|
@ -43,78 +45,98 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
|
||||
levelRoleMap: {},
|
||||
// List of power levels to show in the drop-down
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
maxValue: Infinity,
|
||||
usersDefault: 0,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
LEVEL_ROLE_MAP = Roles.levelRoleMap();
|
||||
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
this._initStateFromProps(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this._initStateFromProps(newProps);
|
||||
},
|
||||
|
||||
_initStateFromProps: function(newProps) {
|
||||
// This needs to be done now because levelRoleMap has translated strings
|
||||
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||
const options = Object.keys(levelRoleMap).filter((l) => {
|
||||
return l === undefined || l <= newProps.maxValue;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
levelRoleMap,
|
||||
options,
|
||||
custom: levelRoleMap[newProps.value] === undefined,
|
||||
});
|
||||
},
|
||||
|
||||
onSelectChange: function(event) {
|
||||
this.setState({ custom: event.target.value === "Custom" });
|
||||
if (event.target.value !== "Custom") {
|
||||
this.props.onChange(this.getValue());
|
||||
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||
this.props.onChange(event.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
onCustomBlur: function(event) {
|
||||
this.props.onChange(this.getValue());
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
},
|
||||
|
||||
onCustomKeyDown: function(event) {
|
||||
if (event.key == "Enter") {
|
||||
this.props.onChange(this.getValue());
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
let value;
|
||||
if (this.refs.select) {
|
||||
value = reverseRoles[this.refs.select.value];
|
||||
if (this.refs.custom) {
|
||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let customPicker;
|
||||
if (this.state.custom) {
|
||||
let input;
|
||||
if (this.props.disabled) {
|
||||
input = <span>{ this.props.value }</span>;
|
||||
customPicker = <span>{ _t(
|
||||
"Custom of %(powerLevel)s",
|
||||
{ powerLevel: this.props.value },
|
||||
) }</span>;
|
||||
} else {
|
||||
input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />;
|
||||
customPicker = <span> = <input
|
||||
ref="custom"
|
||||
type="text"
|
||||
size="3"
|
||||
defaultValue={this.props.value}
|
||||
onBlur={this.onCustomBlur}
|
||||
onKeyDown={this.onCustomKeyDown}
|
||||
/>
|
||||
</span>;
|
||||
}
|
||||
customPicker = <span> of { input }</span>;
|
||||
}
|
||||
|
||||
let selectValue;
|
||||
if (this.state.custom) {
|
||||
selectValue = "Custom";
|
||||
selectValue = "SELECT_VALUE_CUSTOM";
|
||||
} else {
|
||||
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
selectValue = this.state.levelRoleMap[this.props.value] ?
|
||||
this.props.value : "SELECT_VALUE_CUSTOM";
|
||||
}
|
||||
let select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
||||
} else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
// Each level must have a definition in this.state.levelRoleMap
|
||||
let options = this.state.options.map((level) => {
|
||||
return {
|
||||
value: LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
value: level,
|
||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||
};
|
||||
});
|
||||
options.push({ value: "Custom", text: _t("Custom level") });
|
||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||
});
|
||||
|
|
110
src/components/views/elements/SettingsFlag.js
Normal file
110
src/components/views/elements/SettingsFlag.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SettingsFlag',
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
level: React.PropTypes.string.isRequired,
|
||||
roomId: React.PropTypes.string, // for per-room settings
|
||||
label: React.PropTypes.string, // untranslated
|
||||
onChange: React.PropTypes.func,
|
||||
isExplicit: React.PropTypes.bool,
|
||||
manualSave: React.PropTypes.bool,
|
||||
|
||||
// If group is supplied, then this will create a radio button instead.
|
||||
group: React.PropTypes.string,
|
||||
value: React.PropTypes.any, // the value for the radio button
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
onChange: function(e) {
|
||||
if (this.props.group && !e.target.checked) return;
|
||||
|
||||
const newState = this.props.group ? this.props.value : e.target.checked;
|
||||
if (!this.props.manualSave) this.save(newState);
|
||||
else this.setState({ value: newState });
|
||||
if (this.props.onChange) this.props.onChange(newState);
|
||||
},
|
||||
|
||||
save: function(val = undefined) {
|
||||
return SettingsStore.setValue(
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.level,
|
||||
val !== undefined ? val : this.state.value,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
);
|
||||
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
|
||||
// We generate a relatively complex ID to avoid conflicts
|
||||
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
|
||||
let checkbox = (
|
||||
<input id={id}
|
||||
type="checkbox"
|
||||
defaultChecked={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!canChange}
|
||||
/>
|
||||
);
|
||||
if (this.props.group) {
|
||||
checkbox = (
|
||||
<input id={id}
|
||||
type="radio"
|
||||
name={this.props.group}
|
||||
value={this.props.value}
|
||||
checked={value === this.props.value}
|
||||
onChange={this.onChange}
|
||||
disabled={!canChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
{ checkbox }
|
||||
{ label }
|
||||
</label>
|
||||
);
|
||||
},
|
||||
});
|
119
src/components/views/elements/TagTile.js
Normal file
119
src/components/views/elements/TagTile.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
// - Rooms that are part of the group
|
||||
// - Direct messages with members of the group
|
||||
// with the intention that this could be expanded to arbitrary tags in future.
|
||||
export default React.createClass({
|
||||
displayName: 'TagTile',
|
||||
|
||||
propTypes: {
|
||||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// Whether the mouse is over the tile
|
||||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.getGroupProfileCached(
|
||||
this.context.matrixClient,
|
||||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: this.props.tag,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
this.setState({hover: false});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 35;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
|
||||
const className = classNames({
|
||||
mx_TagTile: true,
|
||||
mx_TagTile_selected: this.props.selected,
|
||||
});
|
||||
|
||||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
{ tip }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
61
src/components/views/elements/TintableSvgButton.js
Normal file
61
src/components/views/elements/TintableSvgButton.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import TintableSvg from './TintableSvg';
|
||||
|
||||
export default class TintableSvgButton extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = "mx_TintableSvgButton";
|
||||
if (this.props.className) {
|
||||
classes += " " + this.props.className;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
className={classes}>
|
||||
<TintableSvg
|
||||
src={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
></TintableSvg>
|
||||
<span
|
||||
title={this.props.title}
|
||||
onClick={this.props.onClick} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TintableSvgButton.propTypes = {
|
||||
src: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
width: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
TintableSvgButton.defaultProps = {
|
||||
onClick: function() {},
|
||||
};
|
|
@ -59,9 +59,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(
|
||||
this.context.matrixClient, this.props.groupId,
|
||||
);
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
|
|
|
@ -20,17 +20,12 @@ import sdk from '../../../index';
|
|||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'GroupMemberList',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
@ -49,7 +44,7 @@ export default withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this._fetchMembers();
|
||||
});
|
||||
|
@ -92,7 +87,7 @@ export default withMatrixClient(React.createClass({
|
|||
query = (query || "").toLowerCase();
|
||||
if (query) {
|
||||
memberList = memberList.filter((m) => {
|
||||
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1;
|
||||
const matchesName = (m.displayname || "").toLowerCase().includes(query);
|
||||
const matchesId = m.userId.toLowerCase().includes(query);
|
||||
|
||||
if (!matchesName && !matchesId) {
|
||||
|
@ -174,4 +169,4 @@ export default withMatrixClient(React.createClass({
|
|||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
86
src/components/views/groups/GroupPublicityToggle.js
Normal file
86
src/components/views/groups/GroupPublicityToggle.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import { _t } from '../../../languageHandler.js';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupPublicityToggle',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
busy: false,
|
||||
ready: false,
|
||||
isGroupPublicised: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this.setState({
|
||||
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
||||
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onPublicityToggle: function(e) {
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
busy: true,
|
||||
// Optimistic early update
|
||||
isGroupPublicised: !this.state.isGroupPublicised,
|
||||
});
|
||||
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onClick={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
(this.state.isGroupPublicised ?
|
||||
_t("Flair will appear if enabled in room settings") :
|
||||
_t("Flair will not appear")
|
||||
);
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<label onClick={this._onPublicityToggle}>
|
||||
{ input }
|
||||
{ labelText }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -61,9 +61,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(
|
||||
this.context.matrixClient, this.props.groupId,
|
||||
);
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
|
|
|
@ -19,15 +19,10 @@ import sdk from '../../../index';
|
|||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
|
||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||
|
||||
export default React.createClass({
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
@ -46,7 +41,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this._fetchRooms();
|
||||
});
|
||||
|
|
93
src/components/views/groups/GroupTile.js
Normal file
93
src/components/views/groups/GroupTile.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
// Whether to show the short description of the group on the tile
|
||||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst getting cached profile for GroupTile', err);
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.groupId;
|
||||
const avatarHeight = this.props.avatarHeight;
|
||||
const descElement = this.props.showDescription ?
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div>
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
||||
export default GroupTile;
|
89
src/components/views/groups/GroupUserSettings.js
Normal file
89
src/components/views/groups/GroupUserSettings.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import sdk from '../../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupUserSettings',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
error: null,
|
||||
groups: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.context.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.setState({groups: result.groups || [], error: null});
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
this.setState({groups: null, error: err});
|
||||
});
|
||||
},
|
||||
|
||||
_renderGroupPublicity() {
|
||||
let text = "";
|
||||
let scrollbox = <div />;
|
||||
const groups = this.state.groups;
|
||||
|
||||
if (this.state.error) {
|
||||
text = _t('Something went wrong when trying to get your communities.');
|
||||
} else if (groups === null) {
|
||||
text = _t('Loading...');
|
||||
} else if (groups.length > 0) {
|
||||
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
|
||||
const groupPublicityToggles = groups.map((groupId, index) => {
|
||||
return <GroupPublicityToggle key={index} groupId={groupId} />;
|
||||
});
|
||||
text = _t('Display your community flair in rooms configured to show it.');
|
||||
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
|
||||
<GeminiScrollbar>
|
||||
{ groupPublicityToggles }
|
||||
</GeminiScrollbar>
|
||||
</div>;
|
||||
} else {
|
||||
text = _t("You're not currently a member of any communities.");
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>{ _t('Flair') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
{ scrollbox }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render() {
|
||||
const groupPublicity = this._renderGroupPublicity();
|
||||
|
||||
return <div>
|
||||
{ groupPublicity }
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
|
@ -67,10 +67,10 @@ module.exports = React.createClass({
|
|||
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||
// * embedding the captcha in an iframe (if that works)
|
||||
// * using a better captcha lib
|
||||
ReactDOM.render(_tJsx(
|
||||
ReactDOM.render(_t(
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
} else {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
|
|
@ -20,7 +20,7 @@ import url from 'url';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p>
|
||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
return (
|
||||
<div>
|
||||
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
{ msisdn: <i>{ this._msisdn }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
|
|
59
src/components/views/login/LoginPage.js
Normal file
59
src/components/views/login/LoginPage.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginPage',
|
||||
|
||||
render: function() {
|
||||
// FIXME: this should be turned into a proper skin with a StatusLoginPage component
|
||||
if (SettingsStore.getValue("theme") === 'status') {
|
||||
return (
|
||||
<div className="mx_StatusLogin">
|
||||
<div className="mx_StatusLogin_brand">
|
||||
<img src="themes/status/img/logo.svg" alt="Status" width="221" height="53" />
|
||||
</div>
|
||||
<div className="mx_StatusLogin_content">
|
||||
<div className="mx_StatusLogin_header">
|
||||
<h1>Status Community Chat</h1>
|
||||
<div className="mx_StatusLogin_subtitle">
|
||||
A safer, decentralised communication
|
||||
platform <a href="https://riot.im">powered by Riot</a>
|
||||
</div>
|
||||
</div>
|
||||
{ this.props.children }
|
||||
<div className="mx_StatusLogin_footer">
|
||||
<p>This channel is for our development community.</p>
|
||||
<p>Interested in SNT and discussions on the cryptocurrency market?</p>
|
||||
<p><a href="https://t.me/StatusNetworkChat" target="_blank" className="mx_StatusLogin_footer_cta">Join Telegram Chat</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -122,7 +122,7 @@ class PasswordLogin extends React.Component {
|
|||
mx_Login_field_disabled: disabled,
|
||||
};
|
||||
|
||||
switch(loginType) {
|
||||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
return <input
|
||||
|
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
placeholder={_t('User name')}
|
||||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("User name")}
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
|
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Dropdown
|
||||
|
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
|
|||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
|
|
|
@ -22,6 +22,8 @@ import Email from '../../../email';
|
|||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
|
@ -122,7 +124,7 @@ module.exports = React.createClass({
|
|||
password: this.refs.password.value.trim(),
|
||||
email: email,
|
||||
phoneCountry: this.state.phoneCountry,
|
||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -180,7 +182,7 @@ module.exports = React.createClass({
|
|||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber.value;
|
||||
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
|
@ -273,10 +275,14 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const self = this;
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
|
||||
|
||||
const emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder={_t("Email address (optional)")}
|
||||
autoFocus={true} placeholder={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
|
@ -306,28 +312,31 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
const phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix',
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
let phoneSection;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix',
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
|
|
|
@ -25,8 +25,8 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -81,7 +81,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageEnter: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageLeave: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -218,7 +218,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
|
@ -230,6 +230,7 @@ module.exports = React.createClass({
|
|||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
|
|
|
@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -151,7 +151,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -67,24 +67,17 @@ module.exports = React.createClass({
|
|||
'crop',
|
||||
);
|
||||
|
||||
// it sucks that _tJsx doesn't support normal _t substitutions :((
|
||||
return (
|
||||
<div className="mx_RoomAvatarEvent">
|
||||
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||
[
|
||||
/%\(senderDisplayName\)s/,
|
||||
/<img\/>/,
|
||||
],
|
||||
[
|
||||
(sub) => senderDisplayName,
|
||||
(sub) =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={this.onAvatarClick.bind(this, name)}>
|
||||
<BaseAvatar width={14} height={14} url={url}
|
||||
name={name} />
|
||||
</AccessibleButton>,
|
||||
],
|
||||
)
|
||||
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||
{ senderDisplayName: senderDisplayName },
|
||||
{
|
||||
'img': () =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={this.onAvatarClick.bind(this, name)}>
|
||||
<BaseAvatar width={14} height={14} url={url} name={name} />
|
||||
</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,53 +17,128 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import { _tJsx } from '../../../languageHandler';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function SenderProfile(props) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = props;
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
export default React.createClass({
|
||||
displayName: 'SenderProfile',
|
||||
propTypes: {
|
||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
||||
text: PropTypes.string, // Text to show. Defaults to sender name
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
// Name + flair
|
||||
const nameElem = [
|
||||
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
|
||||
props.enableFlair ?
|
||||
<Flair key='flair'
|
||||
getInitialState() {
|
||||
return {
|
||||
userGroups: null,
|
||||
relatedGroups: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context.matrixClient, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
|
||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
onRoomStateEvents(event) {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
) {
|
||||
this._updateRelatedGroups();
|
||||
}
|
||||
},
|
||||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.mxEvent.getRoomId())
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || []
|
||||
: [],
|
||||
});
|
||||
},
|
||||
|
||||
_getDisplayedGroups(userGroups, relatedGroups) {
|
||||
let displayedGroups = userGroups || [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
displayedGroups = displayedGroups.filter((groupId) => {
|
||||
return relatedGroups.includes(groupId);
|
||||
});
|
||||
} else {
|
||||
displayedGroups = [];
|
||||
}
|
||||
return displayedGroups;
|
||||
},
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair = <div />;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
);
|
||||
|
||||
// Backwards-compatible replacing of "(IRC)" with AS user flair
|
||||
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
|
||||
|
||||
flair = <Flair key='flair'
|
||||
userId={mxEvent.getSender()}
|
||||
roomId={mxEvent.getRoomId()}
|
||||
showRelated={true} />
|
||||
: null,
|
||||
];
|
||||
groups={displayedGroups}
|
||||
/>;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
|
||||
|
||||
if(props.text) {
|
||||
// Replace senderName, and wrap surrounding text in spans with the right class
|
||||
content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [
|
||||
p1 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
|
||||
nameElem,
|
||||
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
|
||||
]);
|
||||
} else {
|
||||
content = nameElem;
|
||||
}
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
<span className="mx_SenderProfile_name">
|
||||
{ nameElem }
|
||||
</span>
|
||||
{ flair }
|
||||
</span>;
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const content = this.props.text ?
|
||||
<span className="mx_SenderProfile_aux">
|
||||
{ _t(this.props.text, { senderName: () => nameElem }) }
|
||||
</span> : nameFlair;
|
||||
|
||||
SenderProfile.propTypes = {
|
||||
mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing
|
||||
text: React.PropTypes.string, // Text to show. Defaults to sender name
|
||||
onClick: React.PropTypes.func,
|
||||
};
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,11 +29,11 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
|||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
|
@ -169,7 +169,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
let pillified = false;
|
||||
|
@ -419,7 +419,7 @@ module.exports = React.createClass({
|
|||
const content = mxEvent.getContent();
|
||||
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false),
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
});
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
|
|
|
@ -253,7 +253,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>Addresses</h3>
|
||||
<h3>{ _t('Addresses') }</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||
</div>
|
||||
|
|
|
@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
const Modal = require("../../../Modal");
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
const ROOM_COLORS = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
|
||||
["#81bddb", "#eaf1f4"],
|
||||
["#bd79cb", "#f3eaf5"],
|
||||
["#c65d94", "#f5eaef"],
|
||||
|
@ -47,17 +48,17 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
const data = {
|
||||
index: 0,
|
||||
primary_color: ROOM_COLORS[0].primary_color,
|
||||
secondary_color: ROOM_COLORS[0].secondary_color,
|
||||
primary_color: ROOM_COLORS[0][0],
|
||||
secondary_color: ROOM_COLORS[0][1],
|
||||
hasChanged: false,
|
||||
};
|
||||
const event = this.props.room.getAccountData("org.matrix.room.color_scheme");
|
||||
if (!event) {
|
||||
return data;
|
||||
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
|
||||
|
||||
if (scheme.primary_color && scheme.secondary_color) {
|
||||
// We only use the user's scheme if the scheme is valid.
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
}
|
||||
const scheme = event.getContent();
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
data.index = this._getColorIndex(data);
|
||||
|
||||
if (data.index === -1) {
|
||||
|
@ -81,13 +82,13 @@ module.exports = React.createClass({
|
|||
// We would like guests to be able to set room colour but currently
|
||||
// they can't, so we still send the request but display a sensible
|
||||
// error if it fails.
|
||||
return MatrixClientPeg.get().setRoomAccountData(
|
||||
this.props.room.roomId, "org.matrix.room.color_scheme", {
|
||||
primary_color: this.state.primary_color,
|
||||
secondary_color: this.state.secondary_color,
|
||||
},
|
||||
).catch(function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
// TODO: Support guests for room color. Technically this is possible via granular settings
|
||||
// Granular settings would mean the guest is forced to use the DEVICE level though.
|
||||
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
|
||||
primary_color: this.state.primary_color,
|
||||
secondary_color: this.state.secondary_color,
|
||||
}).catch(function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -104,8 +104,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return (<div>
|
||||
<h3>{ _t('Related Communities') }</h3>
|
||||
return <div>
|
||||
<h3>{ _t('Flair') }</h3>
|
||||
<EditableItemList
|
||||
items={this.state.newGroupsList}
|
||||
className={"mx_RelatedGroupSettings"}
|
||||
|
@ -115,12 +115,12 @@ module.exports = React.createClass({
|
|||
onItemAdded={this.onGroupAdded}
|
||||
onItemEdited={this.onGroupEdited}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Related communities for this room:')}
|
||||
noItemsLabel={_t('This room has no related communities')}
|
||||
itemsLabel={_t('Showing flair for these communities:')}
|
||||
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||
placeholder={_t(
|
||||
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||
)}
|
||||
/>
|
||||
</div>);
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,13 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const React = require('react');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require("../../../index");
|
||||
const Modal = require("../../../Modal");
|
||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -30,137 +28,64 @@ module.exports = React.createClass({
|
|||
room: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomState = this.props.room.currentState;
|
||||
|
||||
const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
|
||||
const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
|
||||
|
||||
return {
|
||||
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
|
||||
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
|
||||
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.originalState = Object.assign({}, this.state);
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
const promises = [];
|
||||
|
||||
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
|
||||
promises.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId, "org.matrix.room.preview_urls", {
|
||||
disable: this.state.globalDisableUrlPreview,
|
||||
}, "",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let content = undefined;
|
||||
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
|
||||
content = this.state.userDisableUrlPreview ? { disable: true } : {};
|
||||
}
|
||||
|
||||
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
|
||||
if (!content || content.disable === undefined) {
|
||||
content = this.state.userEnableUrlPreview ? { disable: false } : {};
|
||||
}
|
||||
}
|
||||
|
||||
if (content) {
|
||||
promises.push(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
this.props.room.roomId, "org.matrix.room.preview_urls", content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
|
||||
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
|
||||
return promises;
|
||||
},
|
||||
|
||||
onGlobalDisableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
|
||||
});
|
||||
},
|
||||
|
||||
onUserEnableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
userDisableUrlPreview: false,
|
||||
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
|
||||
});
|
||||
},
|
||||
|
||||
onUserDisableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
|
||||
userEnableUrlPreview: false,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const self = this;
|
||||
const roomState = this.props.room.currentState;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
|
||||
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
|
||||
let disableRoomPreviewUrls;
|
||||
if (maySetRoomPreviewUrls) {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
<input type="checkbox" ref="globalDisableUrlPreview"
|
||||
onChange={this.onGlobalDisableUrlPreviewChange}
|
||||
checked={this.state.globalDisableUrlPreview} />
|
||||
{ _t("Disable URL previews by default for participants in this room") }
|
||||
</label>;
|
||||
} else {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
{ _t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")}) }
|
||||
</label>;
|
||||
}
|
||||
|
||||
let urlPreviewText = null;
|
||||
if (UserSettingsStore.getUrlPreviewsDisabled()) {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
|
||||
let previewsForRoom = null;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={this.props.room.roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsSelf"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>
|
||||
{ urlPreviewText }
|
||||
</label>
|
||||
{ disableRoomPreviewUrls }
|
||||
<label>
|
||||
<input type="checkbox" ref="userEnableUrlPreview"
|
||||
onChange={this.onUserEnableUrlPreviewChange}
|
||||
checked={this.state.userEnableUrlPreview} />
|
||||
{ _t("Enable URL previews for this room (affects only you)") }
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" ref="userDisableUrlPreview"
|
||||
onChange={this.onUserDisableUrlPreviewChange}
|
||||
checked={this.state.userDisableUrlPreview} />
|
||||
{ _t("Disable URL previews for this room (affects only you)") }
|
||||
</label>
|
||||
<label>{ previewsForAccount }</label>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
@ -131,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;
|
||||
|
||||
|
@ -177,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;
|
||||
}
|
||||
|
@ -224,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}
|
||||
/>);
|
||||
});
|
||||
|
||||
|
|
|
@ -24,9 +24,10 @@ 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;
|
||||
|
@ -95,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) {
|
||||
|
|
|
@ -21,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({
|
||||
|
@ -100,13 +99,13 @@ 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>);
|
||||
}
|
||||
|
|
|
@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils');
|
|||
|
||||
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',
|
||||
};
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.member': '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.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events' : 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
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:
|
||||
|
@ -188,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();
|
||||
},
|
||||
|
@ -206,6 +216,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
|
||||
this.setState({
|
||||
verified: verified,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onWidgetLoad();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -433,7 +446,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// Info messages are basically information about commands processed on a room
|
||||
const isInfoMessage = (eventType !== 'm.room.message');
|
||||
|
||||
const 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) {
|
||||
|
@ -600,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({
|
||||
|
|
|
@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const defaultPerms = {
|
||||
can: {},
|
||||
muted: false,
|
||||
modifyLevel: false,
|
||||
};
|
||||
const room = this.props.matrixClient.getRoom(member.roomId);
|
||||
if (!room) return defaultPerms;
|
||||
|
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_calculateCanPermissions: function(me, them, powerLevels) {
|
||||
const isMe = me.userId === them.userId;
|
||||
const can = {
|
||||
kick: false,
|
||||
ban: false,
|
||||
mute: 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;
|
||||
|
@ -531,16 +532,13 @@ 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 && me.powerLevel >= editPowerLevel;
|
||||
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
|
||||
can.modifyLevelMax = me.powerLevel;
|
||||
|
||||
return can;
|
||||
},
|
||||
|
||||
|
@ -564,7 +562,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
onMemberAvatarClick: function() {
|
||||
const member = this.props.member;
|
||||
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
||||
if(!avatarUrl) return;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
|
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
}
|
||||
|
||||
let roomMemberDetails = null;
|
||||
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');
|
||||
|
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ _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>
|
||||
|
|
|
@ -22,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 {
|
||||
|
@ -49,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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -111,10 +111,10 @@ export default class MessageComposer extends React.Component {
|
|||
</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]);
|
||||
}
|
||||
}
|
||||
|
@ -226,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});
|
||||
}
|
||||
|
||||
|
@ -238,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");
|
||||
|
||||
|
@ -246,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>,
|
||||
);
|
||||
|
||||
|
|
|
@ -28,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, _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';
|
||||
|
@ -50,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$');
|
||||
|
@ -74,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',
|
||||
});
|
||||
|
@ -105,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
|
||||
|
@ -165,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);
|
||||
|
||||
|
@ -216,7 +203,7 @@ 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.findPillEntities.bind(this),
|
||||
component: (entityProps) => {
|
||||
|
@ -384,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,
|
||||
|
@ -431,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
|
||||
|
@ -552,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 => {
|
||||
|
@ -697,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)
|
||||
) {
|
||||
|
|
|
@ -44,6 +44,8 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
// 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;
|
||||
const t = parseInt(time / 1000);
|
||||
|
@ -53,41 +55,39 @@ module.exports = React.createClass({
|
|||
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 _t("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) {
|
||||
const duration = this.getDuration(this.props.activeAgo);
|
||||
const 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -339,7 +339,7 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||
let pinsIndicator = null;
|
||||
if (this._hasUnreadPins()) {
|
||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
||||
|
@ -389,7 +389,7 @@ module.exports = React.createClass({
|
|||
|
||||
let rightRow;
|
||||
let manageIntegsButton;
|
||||
if(this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
manageIntegsButton = <ManageIntegsButton
|
||||
roomId={this.props.room.roomId}
|
||||
/>;
|
||||
|
|
|
@ -18,18 +18,18 @@ limitations under the License.
|
|||
'use strict';
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const CallHandler = require('../../../CallHandler');
|
||||
const RoomListSorter = require("../../../RoomListSorter");
|
||||
const Unread = require('../../../Unread');
|
||||
const dis = require("../../../dispatcher");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
const Rooms = require('../../../Rooms');
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const Receipt = require('../../../utils/Receipt');
|
||||
import FilterStore from '../../../stores/FilterStore';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
|
||||
|
@ -63,6 +63,7 @@ module.exports = React.createClass({
|
|||
totalRoomCount: null,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -70,6 +71,7 @@ module.exports = React.createClass({
|
|||
this.mounted = false;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("deleteRoom", this.onDeleteRoom);
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
|
@ -82,6 +84,35 @@ module.exports = React.createClass({
|
|||
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();
|
||||
|
||||
// order of the sublists
|
||||
|
@ -150,6 +181,16 @@ module.exports = React.createClass({
|
|||
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();
|
||||
},
|
||||
|
@ -236,6 +277,45 @@ module.exports = React.createClass({
|
|||
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
|
||||
|
@ -255,9 +335,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
const self = this;
|
||||
const lists = {};
|
||||
|
||||
lists["im.vector.fake.invite"] = [];
|
||||
lists["m.favourite"] = [];
|
||||
lists["im.vector.fake.recent"] = [];
|
||||
|
@ -265,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;
|
||||
|
||||
|
@ -278,13 +355,18 @@ 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())) {
|
||||
// Used to split rooms via tags
|
||||
const tagNames = Object.keys(room.tags);
|
||||
|
||||
// Apply TagPanel filtering, derived from FilterStore
|
||||
if (!this.isRoomInSelectedTags(room)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tagNames.length) {
|
||||
for (let i = 0; i < tagNames.length; i++) {
|
||||
const tagName = tagNames[i];
|
||||
|
@ -476,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) {
|
||||
|
@ -486,28 +572,25 @@ 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>;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ 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',
|
||||
|
@ -135,13 +135,13 @@ 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 }
|
||||
|
@ -165,13 +165,13 @@ module.exports = React.createClass({
|
|||
|
||||
let actionText;
|
||||
if (kicked) {
|
||||
if(roomName) {
|
||||
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) {
|
||||
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});
|
||||
|
@ -211,9 +211,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
|
||||
<br />
|
||||
{ _tJsx("<a>Click here</a> to join the discussion!",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>,
|
||||
{ _t("<a>Click here</a> to join the discussion!",
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
|||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import { _t, _tJsx, _td } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
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
|
||||
|
@ -311,7 +311,7 @@ module.exports = React.createClass({
|
|||
// url preview settings
|
||||
const ps = this.saveUrlPreviewSettings();
|
||||
if (ps.length > 0) {
|
||||
promises.push(ps);
|
||||
ps.map((p) => promises.push(p));
|
||||
}
|
||||
|
||||
// related groups
|
||||
|
@ -363,26 +363,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
saveBlacklistUnverifiedDevicesPerRoom: function() {
|
||||
if (!this.refs.blacklistUnverified) return;
|
||||
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
|
||||
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
|
||||
}
|
||||
},
|
||||
|
||||
_isRoomBlacklistUnverified: function() {
|
||||
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
|
||||
if (blacklistUnverifiedDevicesPerRoom) {
|
||||
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_setRoomBlacklistUnverified: function(value) {
|
||||
const 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) {
|
||||
|
@ -588,19 +578,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_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 isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
|
||||
const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
|
||||
|
||||
const 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>;
|
||||
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 (
|
||||
|
@ -637,9 +628,7 @@ module.exports = React.createClass({
|
|||
const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomState = this.props.room.currentState;
|
||||
|
@ -758,9 +747,9 @@ 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 ?
|
||||
(tags.map(function(tag, i) {
|
||||
|
@ -790,10 +779,10 @@ 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>;
|
||||
}
|
||||
|
@ -910,41 +899,41 @@ module.exports = React.createClass({
|
|||
<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, 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} />
|
||||
<PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
|
||||
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
|
||||
<PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
|
||||
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
|
||||
<PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
|
||||
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
|
||||
<PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
|
||||
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
|
||||
<PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
|
||||
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
|
||||
<PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
|
||||
</div>
|
||||
|
||||
{ Object.keys(events_levels).map(function(event_type, i) {
|
||||
let label = plEventsToLabels[event_type];
|
||||
if (label) label = _t(label);
|
||||
else label = _tJsx("To send events of type <eventType/>, you must be a", /<eventType\/>/, () => <code>{ event_type }</code>);
|
||||
else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
|
||||
return (
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
|
||||
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} onChange={self.onPowerLevelsChanged}
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
|||
const RoomNotifs = require('../../../RoomNotifs');
|
||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
||||
|
@ -55,7 +54,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return({
|
||||
return ({
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default class DevicesPanel extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
@ -29,11 +29,16 @@ export default class DevicesPanel extends React.Component {
|
|||
this.state = {
|
||||
devices: undefined,
|
||||
deviceLoadError: undefined,
|
||||
|
||||
selectedDevices: [],
|
||||
deleting: false,
|
||||
};
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._renderDevice = this._renderDevice.bind(this);
|
||||
this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -82,25 +87,78 @@ export default class DevicesPanel extends React.Component {
|
|||
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
||||
}
|
||||
|
||||
_onDeviceDeleted(device) {
|
||||
_onDeviceSelectionToggled(device) {
|
||||
if (this._unmounted) { return; }
|
||||
|
||||
// delete the removed device from our list.
|
||||
const removed_id = device.device_id;
|
||||
const deviceId = device.device_id;
|
||||
this.setState((state, props) => {
|
||||
const newDevices = state.devices.filter(
|
||||
(d) => { return d.device_id != removed_id; },
|
||||
);
|
||||
return { devices: newDevices };
|
||||
// Make a copy of the selected devices, then add or remove the device
|
||||
const selectedDevices = state.selectedDevices.slice();
|
||||
|
||||
const i = selectedDevices.indexOf(deviceId);
|
||||
if (i === -1) {
|
||||
selectedDevices.push(deviceId);
|
||||
} else {
|
||||
selectedDevices.splice(i, 1);
|
||||
}
|
||||
|
||||
return {selectedDevices};
|
||||
});
|
||||
}
|
||||
|
||||
_onDeleteClick() {
|
||||
this.setState({
|
||||
deleting: true,
|
||||
});
|
||||
|
||||
this._makeDeleteRequest(null).catch((error) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
throw error;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
|
||||
title: _t("Authentication"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: error.data,
|
||||
makeRequest: this._makeDeleteRequest.bind(this),
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error("Error deleting devices", e);
|
||||
if (this._unmounted) { return; }
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
deleting: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_makeDeleteRequest(auth) {
|
||||
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
|
||||
() => {
|
||||
// Remove the deleted devices from `devices`, reset selection to []
|
||||
this.setState({
|
||||
devices: this.state.devices.filter(
|
||||
(d) => !this.state.selectedDevices.includes(d.device_id),
|
||||
),
|
||||
selectedDevices: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_renderDevice(device) {
|
||||
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||
return (
|
||||
<DevicesPanelEntry key={device.device_id} device={device}
|
||||
onDeleted={()=>{this._onDeviceDeleted(device);}} />
|
||||
);
|
||||
return <DevicesPanelEntry
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
selected={this.state.selectedDevices.includes(device.device_id)}
|
||||
onDeviceToggled={this._onDeviceSelectionToggled}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -124,6 +182,12 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
devices.sort(this._deviceCompare);
|
||||
|
||||
const deleteButton = this.state.deleting ?
|
||||
<Spinner w={22} h={22} /> :
|
||||
<div className="mx_textButton" onClick={this._onDeleteClick}>
|
||||
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
|
||||
</div>;
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
<div className={classes}>
|
||||
|
@ -131,7 +195,9 @@ export default class DevicesPanel extends React.Component {
|
|||
<div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div>
|
||||
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
|
||||
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons"></div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
{ this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') }
|
||||
</div>
|
||||
</div>
|
||||
{ devices.map(this._renderDevice) }
|
||||
</div>
|
||||
|
|
|
@ -19,24 +19,15 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import DateUtils from '../../../DateUtils';
|
||||
|
||||
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleting: false,
|
||||
deleteError: undefined,
|
||||
};
|
||||
|
||||
this._unmounted = false;
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this.onDeviceToggled = this.onDeviceToggled.bind(this);
|
||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -53,56 +44,8 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onDeleteClick() {
|
||||
this.setState({deleting: true});
|
||||
|
||||
if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) {
|
||||
this.context.authCache.auth = null;
|
||||
}
|
||||
|
||||
// try with auth cache (which is null, so no interactive auth, to start off)
|
||||
this._makeDeleteRequest(this.context.authCache.auth).catch((error) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
throw error;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
|
||||
title: _t("Authentication"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: error.data,
|
||||
makeRequest: this._makeDeleteRequest,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
deleting: false,
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error("Error deleting device", e);
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
deleting: false,
|
||||
deleteError: _t("Failed to delete device"),
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
|
||||
_makeDeleteRequest(auth) {
|
||||
this.context.authCache.auth = auth;
|
||||
this.context.authCache.lastUpdate = Date.now();
|
||||
|
||||
const device = this.props.device;
|
||||
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
|
||||
() => {
|
||||
this.props.onDeleted();
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({ deleting: false });
|
||||
},
|
||||
);
|
||||
onDeviceToggled() {
|
||||
this.props.onDeviceToggled(this.props.device);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -110,16 +53,6 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
|
||||
const device = this.props.device;
|
||||
|
||||
if (this.state.deleting) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let lastSeen = "";
|
||||
if (device.last_seen_ts) {
|
||||
const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts));
|
||||
|
@ -127,18 +60,6 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
lastSeenDate.toLocaleString();
|
||||
}
|
||||
|
||||
let deleteButton;
|
||||
if (this.state.deleteError) {
|
||||
deleteButton = <div className="error">{ this.state.deleteError }</div>;
|
||||
} else {
|
||||
deleteButton = (
|
||||
<div className="mx_textButton"
|
||||
onClick={this._onDeleteClick}>
|
||||
{ _t("Delete") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let myDeviceClass = '';
|
||||
if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
|
||||
myDeviceClass = " mx_DevicesPanel_myDevice";
|
||||
|
@ -159,7 +80,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
{ lastSeen }
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
{ deleteButton }
|
||||
<input type="checkbox" onClick={this.onDeviceToggled} checked={this.props.selected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -168,13 +89,9 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
|
||||
DevicesPanelEntry.propTypes = {
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onDeleted: React.PropTypes.func,
|
||||
};
|
||||
|
||||
DevicesPanelEntry.contextTypes = {
|
||||
authCache: React.PropTypes.object,
|
||||
onDeviceToggled: React.PropTypes.func,
|
||||
};
|
||||
|
||||
DevicesPanelEntry.defaultProps = {
|
||||
onDeleted: function() {},
|
||||
onDeviceToggled: function() {},
|
||||
};
|
||||
|
|
|
@ -39,7 +39,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onResize: function(e) {
|
||||
if(this.props.onResize) {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize(e);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VideoView',
|
||||
|
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
|||
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
||||
{ "mx_VideoView_localVideoFeed_flipped":
|
||||
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
||||
},
|
||||
);
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue