Merge branch 'develop' into patch-1
This commit is contained in:
commit
9c3e4a1202
614 changed files with 32351 additions and 4122 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,12 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import Velocity from 'velocity-vector';
|
||||
import 'velocity-vector/velocity.ui';
|
||||
import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore";
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('matrix-react-sdk')
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
const CALLOUT_ANIM_DURATION = 1000;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BottomLeftMenu',
|
||||
|
@ -32,11 +36,24 @@ module.exports = React.createClass({
|
|||
return({
|
||||
directoryHover : false,
|
||||
roomsHover : false,
|
||||
homeHover: false,
|
||||
peopleHover : false,
|
||||
settingsHover : false,
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
this._peopleButton = null;
|
||||
this._directoryButton = null;
|
||||
this._createRoomButton = null;
|
||||
this._lastCallouts = {};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
},
|
||||
|
||||
// Room events
|
||||
onDirectoryClick: function() {
|
||||
dis.dispatch({ action: 'view_room_directory' });
|
||||
|
@ -62,6 +79,19 @@ module.exports = React.createClass({
|
|||
this.setState({ roomsHover: false });
|
||||
},
|
||||
|
||||
// Home button events
|
||||
onHomeClick: function() {
|
||||
dis.dispatch({ action: 'view_home_page' });
|
||||
},
|
||||
|
||||
onHomeMouseEnter: function() {
|
||||
this.setState({ homeHover: true });
|
||||
},
|
||||
|
||||
onHomeMouseLeave: function() {
|
||||
this.setState({ homeHover: false });
|
||||
},
|
||||
|
||||
// People events
|
||||
onPeopleClick: function() {
|
||||
dis.dispatch({ action: 'view_create_chat' });
|
||||
|
@ -88,6 +118,30 @@ module.exports = React.createClass({
|
|||
this.setState({ settingsHover: false });
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
let calloutElement;
|
||||
switch (payload.action) {
|
||||
// Incoming instruction: dance!
|
||||
case 'callout_start_chat':
|
||||
calloutElement = this._peopleButton;
|
||||
break;
|
||||
case 'callout_room_directory':
|
||||
calloutElement = this._directoryButton;
|
||||
break;
|
||||
case 'callout_create_room':
|
||||
calloutElement = this._createRoomButton;
|
||||
break;
|
||||
}
|
||||
if (calloutElement) {
|
||||
const lastCallout = this._lastCallouts[payload.action];
|
||||
const now = Date.now();
|
||||
if (lastCallout == undefined || lastCallout < now - CALLOUT_ANIM_DURATION) {
|
||||
this._lastCallouts[payload.action] = now;
|
||||
Velocity(ReactDOM.findDOMNode(calloutElement), "callout.bounce", CALLOUT_ANIM_DURATION);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get the label/tooltip to show
|
||||
getLabel: function(label, show) {
|
||||
if (show) {
|
||||
|
@ -96,29 +150,45 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_collectPeopleButton: function(e) {
|
||||
this._peopleButton = e;
|
||||
},
|
||||
|
||||
_collectDirectoryButton: function(e) {
|
||||
this._directoryButton = e;
|
||||
},
|
||||
|
||||
_collectCreateRoomButton: function(e) {
|
||||
this._createRoomButton = e;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const HomeButton = sdk.getComponent('elements.HomeButton');
|
||||
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const SettingsButton = sdk.getComponent('elements.SettingsButton');
|
||||
|
||||
return (
|
||||
<div className="mx_BottomLeftMenu">
|
||||
<div className="mx_BottomLeftMenu_options">
|
||||
<div className="mx_BottomLeftMenu_people" onClick={ this.onPeopleClick } onMouseEnter={ this.onPeopleMouseEnter } onMouseLeave={ this.onPeopleMouseLeave } >
|
||||
<TintableSvg src="img/icons-people.svg" width="25" height="25" />
|
||||
{ this.getLabel("Start chat", this.state.peopleHover) }
|
||||
<HomeButton tooltip={true} />
|
||||
<div ref={this._collectPeopleButton}>
|
||||
<StartChatButton tooltip={true} />
|
||||
</div>
|
||||
<div className="mx_BottomLeftMenu_directory" onClick={ this.onDirectoryClick } onMouseEnter={ this.onDirectoryMouseEnter } onMouseLeave={ this.onDirectoryMouseLeave } >
|
||||
<TintableSvg src="img/icons-directory.svg" width="25" height="25"/>
|
||||
{ this.getLabel("Room directory", this.state.directoryHover) }
|
||||
<div ref={this._collectDirectoryButton}>
|
||||
<RoomDirectoryButton tooltip={true} />
|
||||
</div>
|
||||
<div className="mx_BottomLeftMenu_createRoom" onClick={ this.onRoomsClick } onMouseEnter={ this.onRoomsMouseEnter } onMouseLeave={ this.onRoomsMouseLeave } >
|
||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
||||
{ this.getLabel("Create new room", this.state.roomsHover) }
|
||||
</div>
|
||||
<div className="mx_BottomLeftMenu_settings" onClick={ this.onSettingsClick } onMouseEnter={ this.onSettingsMouseEnter } onMouseLeave={ this.onSettingsMouseLeave } >
|
||||
<TintableSvg src="img/icons-settings.svg" width="25" height="25" />
|
||||
{ this.getLabel("Settings", this.state.settingsHover) }
|
||||
<div ref={this._collectCreateRoomButton}>
|
||||
<CreateRoomButton tooltip={true} />
|
||||
</div>
|
||||
<GroupsButton tooltip={true} />
|
||||
<span className="mx_BottomLeftMenu_settings">
|
||||
<SettingsButton tooltip={true} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CompatibilityPage',
|
||||
|
@ -39,23 +40,31 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_CompatibilityPage">
|
||||
<div className="mx_CompatibilityPage_box">
|
||||
<p>Sorry, your browser is <b>not</b> able to run Riot.</p>
|
||||
<p>{ _t("Sorry, your browser is <b>not</b> able to run Riot.", {}, { 'b': (sub) => <b>{sub}</b> }) } </p>
|
||||
<p>
|
||||
Riot uses many advanced browser features, some of which are not
|
||||
available or experimental in your current browser.
|
||||
{ _t("Riot uses many advanced browser features, some of which are not available or experimental in your current browser.") }
|
||||
</p>
|
||||
<p>
|
||||
Please install <a href="https://www.google.com/chrome">Chrome</a> or <a href="https://getfirefox.com">Firefox</a> for
|
||||
the best experience. <a href="http://apple.com/safari">Safari</a> and <a href="http://opera.com">Opera</a> work too.
|
||||
{ _t('Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.',
|
||||
{},
|
||||
{
|
||||
'chromeLink': (sub) => <a href="https://www.google.com/chrome">{sub}</a>,
|
||||
'firefoxLink': (sub) => <a href="https://getfirefox.com">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
{ _t('<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.',
|
||||
{},
|
||||
{
|
||||
'safariLink': (sub) => <a href="http://apple.com/safari">{sub}</a>,
|
||||
'operaLink': (sub) => <a href="http://opera.com">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
With your current browser, the look and feel of the application may
|
||||
be completely incorrect, and some or all features may not function.
|
||||
If you want to try it anyway you can continue, but you are on your own
|
||||
in terms of any issues you may encounter!
|
||||
{ _t("With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!") }
|
||||
</p>
|
||||
<button onClick={this.onAccept}>
|
||||
I understand the risks and wish to continue
|
||||
{ _t("I understand the risks and wish to continue") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
110
src/components/structures/HomePage.js
Normal file
110
src/components/structures/HomePage.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import request from 'browser-request';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'HomePage',
|
||||
|
||||
propTypes: {
|
||||
// URL base of the team server. Optional.
|
||||
teamServerUrl: React.PropTypes.string,
|
||||
// Team token. Optional. If set, used to get the static homepage of the team
|
||||
// associated. If unset, homePageUrl will be used.
|
||||
teamToken: React.PropTypes.string,
|
||||
// URL to use as the iFrame src. Defaults to /home.html.
|
||||
homePageUrl: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
iframeSrc: '',
|
||||
page: '',
|
||||
};
|
||||
},
|
||||
|
||||
translate: function(s) {
|
||||
s = sanitizeHtml(_t(s));
|
||||
// ugly fix for https://github.com/vector-im/riot-web/issues/4243
|
||||
s = s.replace(/Riot\.im/, '<a href="https://riot.im" target="_blank" rel="noopener">Riot.im</a>');
|
||||
s = s.replace(/\[matrix\]/, '<a href="https://matrix.org" target="_blank" rel="noopener"><img width="79" height="34" alt="[matrix]" style="padding-left: 1px;vertical-align: middle" src="home/images/matrix.svg"/></a>');
|
||||
return s;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
if (this.props.teamToken && this.props.teamServerUrl) {
|
||||
this.setState({
|
||||
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
|
||||
});
|
||||
}
|
||||
else {
|
||||
// we use request() to inline the homepage into the react component
|
||||
// so that it can inherit CSS and theming easily rather than mess around
|
||||
// with iframes and trying to synchronise document.stylesheets.
|
||||
|
||||
let src = this.props.homePageUrl || 'home.html';
|
||||
|
||||
request(
|
||||
{ method: "GET", url: src },
|
||||
(err, response, body) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.status < 200 || response.status >= 300) {
|
||||
console.warn(`Error loading home page: ${err}`);
|
||||
this.setState({ page: _t("Couldn't load home page") });
|
||||
return;
|
||||
}
|
||||
|
||||
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
|
||||
this.setState({ page: body });
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.iframeSrc) {
|
||||
return (
|
||||
<div className="mx_HomePage">
|
||||
<iframe src={ this.state.iframeSrc } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<GeminiScrollbar autoshow={true} className="mx_HomePage">
|
||||
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -16,57 +16,139 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var DragDropContext = require('react-dnd').DragDropContext;
|
||||
var HTML5Backend = require('react-dnd-html5-backend');
|
||||
var sdk = require('matrix-react-sdk')
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
|
||||
var VectorConferenceHandler = require('../../VectorConferenceHandler');
|
||||
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||
import CallHandler from 'matrix-react-sdk/lib/CallHandler';
|
||||
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
|
||||
var LeftPanel = React.createClass({
|
||||
displayName: 'LeftPanel',
|
||||
|
||||
// NB. If you add props, don't forget to update
|
||||
// shouldComponentUpdate!
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
showCallElement: null,
|
||||
searchFilter: '',
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
componentWillMount: function() {
|
||||
this.focusedElement = null;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this._recheckCallElement(newProps.selectedRoom);
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
// MatrixChat will update whenever the user switches
|
||||
// rooms, but propagating this change all the way down
|
||||
// the react tree is quite slow, so we cut this off
|
||||
// here. The RoomTiles listen for the room change
|
||||
// events themselves to know when to update.
|
||||
// We just need to update if any of these things change.
|
||||
if (
|
||||
this.props.collapsed !== nextProps.collapsed ||
|
||||
this.props.disabled !== nextProps.disabled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.state.searchFilter !== nextState.searchFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
_onFocus: function(ev) {
|
||||
this.focusedElement = ev.target;
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state':
|
||||
this._recheckCallElement(this.props.selectedRoom);
|
||||
_onBlur: function(ev) {
|
||||
this.focusedElement = null;
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
let handled = false;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
this._onMoveFocus(true);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
this._onMoveFocus(false);
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_recheckCallElement: function(selectedRoomId) {
|
||||
// if we aren't viewing a room with an ongoing call, but there is an
|
||||
// active call, show the call element - we need to do this to make
|
||||
// audio/video not crap out
|
||||
var activeCall = CallHandler.getAnyActiveCall();
|
||||
var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
|
||||
var showCall = (activeCall && activeCall.call_state === 'connected' && !callForRoom);
|
||||
this.setState({
|
||||
showCallElement: showCall
|
||||
});
|
||||
_onMoveFocus: function(up) {
|
||||
var element = this.focusedElement;
|
||||
|
||||
// unclear why this isn't needed
|
||||
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||
// this.focusDirection = up;
|
||||
|
||||
var descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
var classes;
|
||||
|
||||
do {
|
||||
var child = up ? element.lastElementChild : element.firstElementChild;
|
||||
var sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
}
|
||||
else if (sibling) {
|
||||
element = sibling;
|
||||
}
|
||||
else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
}
|
||||
else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
if (classes.contains("mx_LeftPanel")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
}
|
||||
|
||||
} while(element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_SearchBox_search") ||
|
||||
classes.contains("mx_RoomSubList_ellipsis")));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.focusedElement = element;
|
||||
this.focusedDescending = descending;
|
||||
}
|
||||
},
|
||||
|
||||
onHideClick: function() {
|
||||
|
@ -75,52 +157,37 @@ var LeftPanel = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onCallViewClick: function() {
|
||||
var call = CallHandler.getAnyActiveCall();
|
||||
if (call) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: call.groupRoomId || call.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onSearch: function(term) {
|
||||
this.setState({ searchFilter: term });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomList = sdk.getComponent('rooms.RoomList');
|
||||
var BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
||||
var SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const RoomList = sdk.getComponent('rooms.RoomList');
|
||||
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
var collapseButton;
|
||||
var classes = "mx_LeftPanel mx_fadable";
|
||||
if (this.props.collapsed) {
|
||||
classes += " collapsed";
|
||||
}
|
||||
else {
|
||||
// Hide the collapse button until we work out how to display it in the new skin
|
||||
// collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
|
||||
let topBox;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
const LoginBox = sdk.getComponent('structures.LoginBox');
|
||||
topBox = <LoginBox collapsed={ this.props.collapsed }/>;
|
||||
} else {
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
}
|
||||
|
||||
var callPreview;
|
||||
if (this.state.showCallElement && !this.props.collapsed) {
|
||||
var CallView = sdk.getComponent('voip.CallView');
|
||||
callPreview = (
|
||||
<CallView
|
||||
className="mx_LeftPanel_callView" showVoice={true} onClick={this.onCallViewClick}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
);
|
||||
}
|
||||
let classes = classNames(
|
||||
"mx_LeftPanel", "mx_fadable",
|
||||
{
|
||||
"collapsed": this.props.collapsed,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className={classes} style={{ opacity: this.props.opacity }}>
|
||||
<SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />
|
||||
{ collapseButton }
|
||||
{ callPreview }
|
||||
<aside className={classes} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
{ topBox }
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<RoomList
|
||||
selectedRoom={this.props.selectedRoom}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
|
@ -130,4 +197,4 @@ var LeftPanel = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
module.exports = DragDropContext(HTML5Backend)(LeftPanel);
|
||||
module.exports = LeftPanel;
|
||||
|
|
93
src/components/structures/LoginBox.js
Normal file
93
src/components/structures/LoginBox.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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
var sdk = require('matrix-react-sdk')
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc');
|
||||
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginBox',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
onToggleCollapse: function(show) {
|
||||
if (show) {
|
||||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
},
|
||||
|
||||
onRegisterClick: function() {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
var toggleCollapse;
|
||||
if (this.props.collapsed) {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="Expand panel"/>
|
||||
</AccessibleButton>
|
||||
}
|
||||
else {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="Collapse panel"/>
|
||||
</AccessibleButton>
|
||||
}
|
||||
|
||||
var loginButton;
|
||||
if (!this.props.collapsed) {
|
||||
loginButton = (
|
||||
<div className="mx_LoginBox_loginButton_wrapper">
|
||||
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
|
||||
{ _t("Login") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
|
||||
{ _t("Register") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox mx_LoginBox">
|
||||
{ loginButton }
|
||||
{ toggleCollapse }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,125 +16,178 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import Analytics from 'matrix-react-sdk/lib/Analytics';
|
||||
import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
|
||||
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from 'matrix-react-sdk/lib/GroupAddressPicker';
|
||||
import GroupStoreCache from 'matrix-react-sdk/lib/stores/GroupStoreCache';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
|
||||
var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc');
|
||||
var Modal = require('matrix-react-sdk/lib/Modal');
|
||||
import { formatCount } from 'matrix-react-sdk/lib/utils/FormattingUtils';
|
||||
|
||||
class HeaderButton extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
Analytics.trackEvent(...this.props.analytics);
|
||||
dis.dispatch({
|
||||
action: 'view_right_panel_phase',
|
||||
phase: this.props.clickPhase,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
return <AccessibleButton
|
||||
aria-label={this.props.title}
|
||||
title={this.props.title}
|
||||
className="mx_RightPanel_headerButton"
|
||||
onClick={this.onClick} >
|
||||
|
||||
<div className="mx_RightPanel_headerButton_badge">
|
||||
{ this.props.badge ? this.props.badge : <span> </span> }
|
||||
</div>
|
||||
<TintableSvg src={this.props.iconSrc} width="25" height="25"/>
|
||||
{ this.props.isHighlighted ? <div className="mx_RightPanel_headerButton_highlight"></div> : <div/> }
|
||||
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
||||
|
||||
HeaderButton.propTypes = {
|
||||
// Whether this button is highlighted
|
||||
isHighlighted: PropTypes.bool.isRequired,
|
||||
// The phase to swap to when the button is clicked
|
||||
clickPhase: PropTypes.string.isRequired,
|
||||
// The source file of the icon to display
|
||||
iconSrc: PropTypes.string.isRequired,
|
||||
|
||||
// The badge to display above the icon
|
||||
badge: PropTypes.node,
|
||||
// The parameters to track the click event
|
||||
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
|
||||
// Button title
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RightPanel',
|
||||
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string, // if showing an orphaned MemberInfo page, this is set
|
||||
// TODO: We're trying to move away from these being props, but we need to know
|
||||
// whether we should be displaying a room or group member list
|
||||
roomId: React.PropTypes.string, // if showing panels for a given room, this is set
|
||||
groupId: React.PropTypes.string, // if showing panels for a given group, this is set
|
||||
collapsed: React.PropTypes.bool, // currently unused property to request for a minimized view of the panel
|
||||
},
|
||||
|
||||
Phase : {
|
||||
MemberList: 'MemberList',
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
Phase: {
|
||||
RoomMemberList: 'RoomMemberList',
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
GroupRoomInfo: 'GroupRoomInfo',
|
||||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
MemberInfo: 'MemberInfo',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = this.context.matrixClient;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
if (this.context.matrixClient) {
|
||||
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
if (this.props.userId) {
|
||||
var member = new Matrix.RoomMember(null, this.props.userId);
|
||||
return {
|
||||
phase: this.Phase.MemberInfo,
|
||||
member: member,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
phase: this.Phase.MemberList
|
||||
}
|
||||
return {
|
||||
phase: this.props.groupId ? this.Phase.GroupMemberList : this.Phase.RoomMemberList,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore();
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
onMemberListButtonClick: function() {
|
||||
if (this.props.collapsed || this.state.phase !== this.Phase.MemberList) {
|
||||
this.setState({ phase: this.Phase.MemberList });
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
_initGroupStore(groupId) {
|
||||
if (!groupId) return;
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
_unregisterGroupStore() {
|
||||
if (this._groupStore) {
|
||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
onFileListButtonClick: function() {
|
||||
if (this.props.collapsed || this.state.phase !== this.Phase.FilePanel) {
|
||||
this.setState({ phase: this.Phase.FilePanel });
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
}
|
||||
onGroupStoreUpdated: function() {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
||||
});
|
||||
},
|
||||
|
||||
onNotificationListButtonClick: function() {
|
||||
if (this.props.collapsed || this.state.phase !== this.Phase.NotificationPanel) {
|
||||
this.setState({ phase: this.Phase.NotificationPanel });
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
}
|
||||
onCollapseClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
},
|
||||
|
||||
onInviteButtonClick: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't invite users. Please register to invite."
|
||||
});
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
// call ChatInviteDialog
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.roomId,
|
||||
});
|
||||
if (this.state.phase === this.Phase.GroupMemberList) {
|
||||
showGroupInviteDialog(this.props.groupId);
|
||||
} else if (this.state.phase === this.Phase.GroupRoomList) {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
} else {
|
||||
// call AddressPickerDialog
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase == this.Phase.MemberList && member.roomId === this.props.roomId) {
|
||||
if (this.state.phase == this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
this._delayedUpdate();
|
||||
}
|
||||
else if (this.state.phase === this.Phase.MemberInfo && member.roomId === this.props.roomId &&
|
||||
else if (this.state.phase === this.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
|
@ -150,107 +205,168 @@ module.exports = React.createClass({
|
|||
});
|
||||
if (payload.member) {
|
||||
this.setState({
|
||||
phase: this.Phase.MemberInfo,
|
||||
phase: this.Phase.RoomMemberInfo,
|
||||
member: payload.member,
|
||||
});
|
||||
} else {
|
||||
if (this.props.roomId) {
|
||||
this.setState({
|
||||
phase: this.Phase.RoomMemberList,
|
||||
});
|
||||
} else if (this.props.groupId) {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberList,
|
||||
member: payload.member,
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
phase: this.Phase.MemberList
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (payload.action === "view_room") {
|
||||
if (this.state.phase === this.Phase.MemberInfo) {
|
||||
this.setState({
|
||||
phase: this.Phase.MemberList
|
||||
});
|
||||
}
|
||||
} else if (payload.action === "view_group") {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberList,
|
||||
member: null,
|
||||
});
|
||||
} else if (payload.action === "view_group_room") {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupRoomInfo,
|
||||
groupRoomId: payload.groupRoomId,
|
||||
});
|
||||
} else if (payload.action === "view_group_room_list") {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupRoomList,
|
||||
});
|
||||
} else if (payload.action === "view_group_user") {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberInfo,
|
||||
member: payload.member,
|
||||
});
|
||||
} else if (payload.action === "view_room") {
|
||||
this.setState({
|
||||
phase: this.Phase.RoomMemberList,
|
||||
});
|
||||
} else if (payload.action === "view_right_panel_phase") {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var MemberList = sdk.getComponent('rooms.MemberList');
|
||||
var NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
var FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var buttonGroup;
|
||||
var inviteGroup;
|
||||
var panel;
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
var filesHighlight;
|
||||
var membersHighlight;
|
||||
var notificationsHighlight;
|
||||
if (!this.props.collapsed) {
|
||||
if (this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) {
|
||||
membersHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
|
||||
}
|
||||
else if (this.state.phase == this.Phase.FilePanel) {
|
||||
filesHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
|
||||
}
|
||||
else if (this.state.phase == this.Phase.NotificationPanel) {
|
||||
notificationsHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
|
||||
}
|
||||
}
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
var membersBadge;
|
||||
if ((this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) && this.props.roomId) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
var room = cli.getRoom(this.props.roomId);
|
||||
var user_is_in_room;
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let inviteGroup;
|
||||
|
||||
let membersBadge;
|
||||
if ((this.state.phase == this.Phase.RoomMemberList || this.state.phase === this.Phase.RoomMemberInfo)
|
||||
&& this.props.roomId
|
||||
) {
|
||||
const cli = this.context.matrixClient;
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let userIsInRoom;
|
||||
if (room) {
|
||||
membersBadge = room.getJoinedMembers().length;
|
||||
user_is_in_room = room.hasMembershipState(
|
||||
MatrixClientPeg.get().credentials.userId, 'join'
|
||||
membersBadge = formatCount(room.getJoinedMembers().length);
|
||||
userIsInRoom = room.hasMembershipState(
|
||||
this.context.matrixClient.credentials.userId, 'join',
|
||||
);
|
||||
}
|
||||
|
||||
if (user_is_in_room) {
|
||||
if (userIsInRoom) {
|
||||
inviteGroup =
|
||||
<div className="mx_RightPanel_invite" onClick={ this.onInviteButtonClick } >
|
||||
<AccessibleButton className="mx_RightPanel_invite" onClick={ this.onInviteButtonClick } >
|
||||
<div className="mx_RightPanel_icon" >
|
||||
<TintableSvg src="img/icon-invite-people.svg" width="35" height="35" />
|
||||
</div>
|
||||
<div className="mx_RightPanel_message">Invite to this room</div>
|
||||
</div>;
|
||||
<div className="mx_RightPanel_message">{ _t('Invite to this room') }</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const isPhaseGroup = [
|
||||
this.Phase.GroupMemberInfo,
|
||||
this.Phase.GroupMemberList
|
||||
].includes(this.state.phase);
|
||||
|
||||
let headerButtons = [];
|
||||
if (this.props.roomId) {
|
||||
buttonGroup =
|
||||
<div className="mx_RightPanel_headerButtonGroup">
|
||||
<div className="mx_RightPanel_headerButton" title="Members" onClick={ this.onMemberListButtonClick }>
|
||||
<div className="mx_RightPanel_headerButton_badge">{ membersBadge ? membersBadge : <span> </span>}</div>
|
||||
<TintableSvg src="img/icons-people.svg" width="25" height="25"/>
|
||||
{ membersHighlight }
|
||||
</div>
|
||||
<div className="mx_RightPanel_headerButton mx_RightPanel_filebutton" title="Files" onClick={ this.onFileListButtonClick }>
|
||||
<div className="mx_RightPanel_headerButton_badge"> </div>
|
||||
<TintableSvg src="img/icons-files.svg" width="25" height="25"/>
|
||||
{ filesHighlight }
|
||||
</div>
|
||||
<div className="mx_RightPanel_headerButton mx_RightPanel_notificationbutton" title="Notifications" onClick={ this.onNotificationListButtonClick }>
|
||||
<div className="mx_RightPanel_headerButton_badge"> </div>
|
||||
<TintableSvg src="img/icons-notifications.svg" width="25" height="25"/>
|
||||
{ notificationsHighlight }
|
||||
</div>
|
||||
</div>;
|
||||
headerButtons = [
|
||||
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
|
||||
isHighlighted={[this.Phase.RoomMemberList, this.Phase.RoomMemberInfo].includes(this.state.phase)}
|
||||
clickPhase={this.Phase.RoomMemberList}
|
||||
badge={membersBadge}
|
||||
analytics={['Right Panel', 'Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/icons-files.svg"
|
||||
isHighlighted={this.state.phase === this.Phase.FilePanel}
|
||||
clickPhase={this.Phase.FilePanel}
|
||||
analytics={['Right Panel', 'File List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/icons-notifications.svg"
|
||||
isHighlighted={this.state.phase === this.Phase.NotificationPanel}
|
||||
clickPhase={this.Phase.NotificationPanel}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
} else if (this.props.groupId) {
|
||||
headerButtons = [
|
||||
<HeaderButton key="_groupMembersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
|
||||
isHighlighted={isPhaseGroup}
|
||||
clickPhase={this.Phase.GroupMemberList}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room.svg"
|
||||
isHighlighted={[this.Phase.GroupRoomList, this.Phase.GroupRoomInfo].includes(this.state.phase)}
|
||||
clickPhase={this.Phase.GroupRoomList}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
if (this.props.roomId || this.props.groupId) {
|
||||
// Hiding the right panel hides it completely and relies on an 'expand' button
|
||||
// being put in the RoomHeader or GroupView header, so only show the minimise
|
||||
// button on these 2 screens or you won't be able to re-expand the panel.
|
||||
headerButtons.push(
|
||||
<div className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
|
||||
title={ _t("Hide panel") } aria-label={ _t("Hide panel") } onClick={ this.onCollapseClick }
|
||||
>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
let panel = <div />;
|
||||
if (!this.props.collapsed) {
|
||||
if(this.props.roomId && this.state.phase == this.Phase.MemberList) {
|
||||
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
|
||||
}
|
||||
else if(this.state.phase == this.Phase.MemberInfo) {
|
||||
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.props.userId} />
|
||||
}
|
||||
else if (this.state.phase == this.Phase.NotificationPanel) {
|
||||
panel = <NotificationPanel />
|
||||
}
|
||||
else if (this.state.phase == this.Phase.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} />
|
||||
if (this.props.roomId && this.state.phase == this.Phase.RoomMemberList) {
|
||||
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
|
||||
} else if (this.props.groupId && this.state.phase == this.Phase.GroupMemberList) {
|
||||
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === this.Phase.GroupRoomList) {
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase == this.Phase.RoomMemberInfo) {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
} else if (this.state.phase == this.Phase.GroupMemberInfo) {
|
||||
panel = <GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id} />;
|
||||
} else if (this.state.phase == this.Phase.GroupRoomInfo) {
|
||||
panel = <GroupRoomInfo
|
||||
groupRoomId={this.state.groupRoomId}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.groupRoomId} />;
|
||||
} else if (this.state.phase == this.Phase.NotificationPanel) {
|
||||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase == this.Phase.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,15 +374,38 @@ module.exports = React.createClass({
|
|||
panel = <div className="mx_RightPanel_blank"></div>;
|
||||
}
|
||||
|
||||
var classes = "mx_RightPanel mx_fadable";
|
||||
if (this.props.collapsed) {
|
||||
classes += " collapsed";
|
||||
if (this.props.groupId && this.state.isUserPrivilegedInGroup) {
|
||||
inviteGroup = isPhaseGroup ? (
|
||||
<AccessibleButton className="mx_RightPanel_invite" onClick={ this.onInviteButtonClick } >
|
||||
<div className="mx_RightPanel_icon" >
|
||||
<TintableSvg src="img/icon-invite-people.svg" width="35" height="35" />
|
||||
</div>
|
||||
<div className="mx_RightPanel_message">{ _t('Invite to this community') }</div>
|
||||
</AccessibleButton>
|
||||
) : (
|
||||
<AccessibleButton className="mx_RightPanel_invite" onClick={ this.onInviteButtonClick } >
|
||||
<div className="mx_RightPanel_icon" >
|
||||
<TintableSvg src="img/icons-room-add.svg" width="35" height="35" />
|
||||
</div>
|
||||
<div className="mx_RightPanel_message">{ _t('Add rooms to this community') }</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let classes = classNames(
|
||||
"mx_RightPanel", "mx_fadable",
|
||||
{
|
||||
"collapsed": this.props.collapsed,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className={classes} style={{ opacity: this.props.opacity }}>
|
||||
<aside className={classes}>
|
||||
<div className="mx_RightPanel_header">
|
||||
{ buttonGroup }
|
||||
<div className="mx_RightPanel_headerButtonGroup">
|
||||
{headerButtons}
|
||||
</div>
|
||||
</div>
|
||||
{ panel }
|
||||
<div className="mx_RightPanel_footer">
|
||||
|
@ -274,6 +413,5 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -23,13 +23,14 @@ var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
|||
var Modal = require('matrix-react-sdk/lib/Modal');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyString = require('linkifyjs/string');
|
||||
var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
var q = require('q');
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
|
||||
|
||||
|
@ -61,6 +62,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
this.scrollPanel = null;
|
||||
|
@ -71,6 +73,7 @@ module.exports = React.createClass({
|
|||
this.protocols = response;
|
||||
this.setState({protocolsLoading: false});
|
||||
}, (err) => {
|
||||
console.warn(`error loading thirdparty protocols: ${err}`);
|
||||
this.setState({protocolsLoading: false});
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// Guests currently aren't allowed to use this API, so
|
||||
|
@ -79,25 +82,29 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to get protocol list from Home Server",
|
||||
description: "The Home Server may be too old to support third party networks",
|
||||
Modal.createTrackedDialog('Failed to get protocol list from Home Server', '', ErrorDialog, {
|
||||
title: _t('Failed to get protocol list from Home Server'),
|
||||
description: _t('The Home Server may be too old to support third party networks'),
|
||||
});
|
||||
});
|
||||
|
||||
// dis.dispatch({
|
||||
// action: 'ui_opacity',
|
||||
// sideOpacity: 0.3,
|
||||
// middleOpacity: 0.3,
|
||||
// action: 'panel_disable',
|
||||
// sideDisabled: true,
|
||||
// middleDisabled: true,
|
||||
// });
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// dis.dispatch({
|
||||
// action: 'ui_opacity',
|
||||
// sideOpacity: 1.0,
|
||||
// middleOpacity: 1.0,
|
||||
// action: 'panel_disable',
|
||||
// sideDisabled: false,
|
||||
// middleDisabled: false,
|
||||
// });
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
refreshRoomList: function() {
|
||||
|
@ -110,7 +117,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getMoreRooms: function() {
|
||||
if (!MatrixClientPeg.get()) return q();
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
|
||||
const my_filter_string = this.state.filterString;
|
||||
const my_server = this.state.roomServer;
|
||||
|
@ -140,6 +147,11 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
||||
this.nextBatch = data.next_batch;
|
||||
this.setState((s) => {
|
||||
s.publicRooms.push(...data.chunk);
|
||||
|
@ -157,12 +169,18 @@ module.exports = React.createClass({
|
|||
// requests either
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: false });
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to get public room list",
|
||||
description: err.message
|
||||
Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, {
|
||||
title: _t('Failed to get public room list'),
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -176,41 +194,42 @@ module.exports = React.createClass({
|
|||
*/
|
||||
removeFromDirectory: function(room) {
|
||||
var alias = get_display_alias_for_room(room);
|
||||
var name = room.name || alias || "Unnamed room";
|
||||
var name = room.name || alias || _t('Unnamed room');
|
||||
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
var desc;
|
||||
if (alias) {
|
||||
desc = `Delete the room alias '${alias}' and remove '${name}' from the directory?`;
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
} else {
|
||||
desc = `Remove '${name}' from the directory?`;
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Remove from Directory",
|
||||
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
||||
title: _t('Remove from Directory'),
|
||||
description: desc,
|
||||
onFinished: (should_delete) => {
|
||||
if (!should_delete) return;
|
||||
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader);
|
||||
var step = `remove '${name}' from the directory.`;
|
||||
var step = _t('remove %(name)s from the directory.', {name: name});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = 'delete the alias.';
|
||||
step = _t('delete the alias.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).done(() => {
|
||||
modal.close();
|
||||
this.refreshRoomList();
|
||||
}, function(err) {
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
this.refreshRoomList();
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to "+step,
|
||||
description: err.toString()
|
||||
console.error("Failed to " + step + ": " + err);
|
||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -247,7 +266,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFillRequest: function(backwards) {
|
||||
if (backwards || !this.nextBatch) return q(false);
|
||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||
|
||||
return this.getMoreRooms();
|
||||
},
|
||||
|
@ -297,9 +316,9 @@ module.exports = React.createClass({
|
|||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
||||
if (!fields) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to join network",
|
||||
description: "Riot does not know how to join a room on this network",
|
||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||
title: _t('Unable to join network'),
|
||||
description: _t('Riot does not know how to join a room on this network'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -308,16 +327,16 @@ module.exports = React.createClass({
|
|||
this.showRoomAlias(resp[0].alias);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Room not found",
|
||||
description: "Couldn't find a matching Matrix room",
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
title: _t('Room not found'),
|
||||
description: _t('Couldn\'t find a matching Matrix room'),
|
||||
});
|
||||
}
|
||||
}, (e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Fetching third party location failed",
|
||||
description: "Unable to look up room ID from server",
|
||||
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
||||
title: _t('Fetching third party location failed'),
|
||||
description: _t('Unable to look up room ID from server'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -335,11 +354,7 @@ module.exports = React.createClass({
|
|||
// to the directory.
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (!room.world_readable && !room.guest_can_join) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Failed to join the room",
|
||||
description: "This room is inaccessible to guests. You may be able to join if you register."
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -352,7 +367,7 @@ module.exports = React.createClass({
|
|||
avatarUrl: room.avatar_url,
|
||||
// XXX: This logic is duplicated from the JS SDK which
|
||||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || "Unnamed room",
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
};
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
|
@ -377,24 +392,24 @@ module.exports = React.createClass({
|
|||
var self = this;
|
||||
var guestRead, guestJoin, perms;
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || "Unnamed room";
|
||||
var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
|
||||
guestRead = null;
|
||||
guestJoin = null;
|
||||
|
||||
if (rooms[i].world_readable) {
|
||||
guestRead = (
|
||||
<div className="mx_RoomDirectory_perm">World readable</div>
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
);
|
||||
}
|
||||
if (rooms[i].guest_can_join) {
|
||||
guestJoin = (
|
||||
<div className="mx_RoomDirectory_perm">Guests can join</div>
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
);
|
||||
}
|
||||
|
||||
perms = null;
|
||||
if (guestRead || guestJoin) {
|
||||
perms = <div className="mx_RoomDirectory_perms">{guestRead} {guestJoin}</div>;
|
||||
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
|
||||
}
|
||||
|
||||
var topic = rooms[i].topic || '';
|
||||
|
@ -459,6 +474,17 @@ module.exports = React.createClass({
|
|||
return fields;
|
||||
},
|
||||
|
||||
/**
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
* We pass it down to the scroll panel.
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (this.scrollPanel) {
|
||||
this.scrollPanel.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -466,7 +492,7 @@ module.exports = React.createClass({
|
|||
if (this.state.protocolsLoading) {
|
||||
return (
|
||||
<div className="mx_RoomDirectory">
|
||||
<SimpleRoomHeader title="Directory" />
|
||||
<SimpleRoomHeader title={ _t('Directory') } />
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
|
@ -484,7 +510,7 @@ module.exports = React.createClass({
|
|||
// request from the scrollpanel because there isn't one
|
||||
let scrollpanel_content;
|
||||
if (rows.length == 0) {
|
||||
scrollpanel_content = <i>No rooms to show</i>;
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
|
@ -518,9 +544,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
|
||||
let placeholder = 'Search for a room';
|
||||
let placeholder = _t('Search for a room');
|
||||
if (!this.state.instanceId) {
|
||||
placeholder = '#example:' + this.state.roomServer;
|
||||
placeholder = _t('#example') + ':' + this.state.roomServer;
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
}
|
||||
|
@ -537,7 +563,7 @@ module.exports = React.createClass({
|
|||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
return (
|
||||
<div className="mx_RoomDirectory">
|
||||
<SimpleRoomHeader title="Directory" />
|
||||
<SimpleRoomHeader title={ _t('Directory') } icon="img/icons-directory.svg" />
|
||||
<div className="mx_RoomDirectory_list">
|
||||
<div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -20,12 +21,16 @@ var React = require('react');
|
|||
var ReactDOM = require('react-dom');
|
||||
var classNames = require('classnames');
|
||||
var DropTarget = require('react-dnd').DropTarget;
|
||||
var sdk = require('matrix-react-sdk')
|
||||
var sdk = require('matrix-react-sdk');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var Unread = require('matrix-react-sdk/lib/Unread');
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs');
|
||||
var FormattingUtils = require('matrix-react-sdk/lib/utils/FormattingUtils');
|
||||
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
var debug = false;
|
||||
|
@ -70,8 +75,8 @@ var RoomSubList = React.createClass({
|
|||
|
||||
order: React.PropTypes.string.isRequired,
|
||||
|
||||
// undefined if no room is selected (eg we are showing settings)
|
||||
selectedRoom: React.PropTypes.string,
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: React.PropTypes.bool,
|
||||
|
||||
startAsHidden: React.PropTypes.bool,
|
||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
|
@ -81,6 +86,9 @@ var RoomSubList = React.createClass({
|
|||
incomingCall: React.PropTypes.object,
|
||||
onShowMoreRooms: React.PropTypes.func,
|
||||
searchFilter: React.PropTypes.string,
|
||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -94,7 +102,9 @@ var RoomSubList = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {}, // NOP
|
||||
onShowMoreRooms: function() {} // NOP
|
||||
onShowMoreRooms: function() {}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -145,12 +155,21 @@ var RoomSubList = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
tsOfNewestEvent: function(room) {
|
||||
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
||||
var ev = room.timeline[i];
|
||||
if (Unread.eventTriggersUnreadCount(ev) ||
|
||||
(ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId))
|
||||
{
|
||||
if (ev.getTs() &&
|
||||
(Unread.eventTriggersUnreadCount(ev) ||
|
||||
(ev.getSender() === MatrixClientPeg.get().credentials.userId))
|
||||
) {
|
||||
return ev.getTs();
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +177,7 @@ var RoomSubList = React.createClass({
|
|||
// we might only have events that don't trigger the unread indicator,
|
||||
// in which case use the oldest event even if normally it wouldn't count.
|
||||
// This is better than just assuming the last event was forever ago.
|
||||
if (room.timeline.length) {
|
||||
if (room.timeline.length && room.timeline[0].getTs()) {
|
||||
return room.timeline[0].getTs();
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
|
@ -227,10 +246,14 @@ var RoomSubList = React.createClass({
|
|||
roomNotificationCount: function(truncateAt) {
|
||||
var self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
}
|
||||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0 || self.props.label === 'Invites';
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
var notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
|
@ -304,43 +327,46 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
calcManualOrderTagData: function(room) {
|
||||
var index = this.state.sortedList.indexOf(room);
|
||||
const index = this.state.sortedList.indexOf(room);
|
||||
|
||||
// we sort rooms by the lexicographic ordering of the 'order' metadata on their tags.
|
||||
// for convenience, we calculate this for now a floating point number between 0.0 and 1.0.
|
||||
|
||||
var orderA = 0.0; // by default we're next to the beginning of the list
|
||||
let orderA = 0.0; // by default we're next to the beginning of the list
|
||||
if (index > 0) {
|
||||
var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
|
||||
const prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
|
||||
if (!prevTag) {
|
||||
console.error("Previous room in sublist is not tagged to be in this list. This should never happen.")
|
||||
}
|
||||
else if (prevTag.order === undefined) {
|
||||
console.error("Previous room in sublist is not tagged to be in this list. This should never happen.");
|
||||
} else if (prevTag.order === undefined) {
|
||||
console.error("Previous room in sublist has no ordering metadata. This should never happen.");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
orderA = prevTag.order;
|
||||
}
|
||||
}
|
||||
|
||||
var orderB = 1.0; // by default we're next to the end of the list too
|
||||
let orderB = 1.0; // by default we're next to the end of the list too
|
||||
if (index < this.state.sortedList.length - 1) {
|
||||
var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
|
||||
const nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
|
||||
if (!nextTag) {
|
||||
console.error("Next room in sublist is not tagged to be in this list. This should never happen.")
|
||||
}
|
||||
else if (nextTag.order === undefined) {
|
||||
console.error("Next room in sublist is not tagged to be in this list. This should never happen.");
|
||||
} else if (nextTag.order === undefined) {
|
||||
console.error("Next room in sublist has no ordering metadata. This should never happen.");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
orderB = nextTag.order;
|
||||
}
|
||||
}
|
||||
|
||||
var order = (orderA + orderB) / 2.0;
|
||||
const order = (orderA + orderB) / 2.0;
|
||||
|
||||
if (order === orderA || order === orderB) {
|
||||
console.error("Cannot describe new list position. This should be incredibly unlikely.");
|
||||
// TODO: renumber the list
|
||||
this.state.sortedList.forEach((room, index) => {
|
||||
MatrixClientPeg.get().setRoomTag(
|
||||
room.roomId, this.props.tagName,
|
||||
{order: index / this.state.sortedList.length},
|
||||
);
|
||||
});
|
||||
return index / this.state.sortedList.length;
|
||||
}
|
||||
|
||||
return order;
|
||||
|
@ -350,7 +376,6 @@ var RoomSubList = React.createClass({
|
|||
var self = this;
|
||||
var DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
|
||||
return this.state.sortedList.map(function(room) {
|
||||
var selected = room.roomId == self.props.selectedRoom;
|
||||
// XXX: is it evil to pass in self as a prop to RoomTile?
|
||||
return (
|
||||
<DNDRoomTile
|
||||
|
@ -358,12 +383,13 @@ var RoomSubList = React.createClass({
|
|||
roomSubList={ self }
|
||||
key={ room.roomId }
|
||||
collapsed={ self.props.collapsed || false}
|
||||
selected={ selected }
|
||||
unread={ Unread.doesRoomHaveUnreadMessages(room) }
|
||||
highlight={ room.getUnreadNotificationCount('highlight') > 0 || self.props.label === 'Invites' }
|
||||
isInvite={ self.props.label === 'Invites' }
|
||||
highlight={ room.getUnreadNotificationCount('highlight') > 0 || self.props.isInvite }
|
||||
isInvite={ self.props.isInvite }
|
||||
refreshSubList={ self._updateSubListCount }
|
||||
incomingCall={ null } />
|
||||
incomingCall={ null }
|
||||
onClick={ self.onRoomTileClick }
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@ -375,7 +401,8 @@ var RoomSubList = React.createClass({
|
|||
var subListNotifCount = subListNotifications[0];
|
||||
var subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
var roomCount = this.props.list.length > 0 ? this.props.list.length : '';
|
||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
|
@ -391,6 +418,9 @@ var RoomSubList = React.createClass({
|
|||
var badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
|
@ -407,7 +437,7 @@ var RoomSubList = React.createClass({
|
|||
if (this.props.incomingCall) {
|
||||
var self = this;
|
||||
// Check if the incoming call is for this section
|
||||
var incomingCallRoom = this.state.sortedList.filter(function(room) {
|
||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
|
@ -417,15 +447,17 @@ var RoomSubList = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<div onClick={ this.onClick } className="mx_RoomSubList_label">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ this.props.collapsed ? '' : this.props.label }
|
||||
<div className="mx_RoomSubList_roomCount">{ roomCount }</div>
|
||||
<div className={chevronClasses}></div>
|
||||
{ badge }
|
||||
{ incomingCall }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -447,11 +479,11 @@ var RoomSubList = React.createClass({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line"></div>
|
||||
<div className="mx_RoomSubList_more">more</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
</div>
|
||||
<div className="mx_RoomSubList_more">{ _t("more") }</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -491,11 +523,12 @@ var RoomSubList = React.createClass({
|
|||
if (list[i].tags[self.props.tagName] && list[i].tags[self.props.tagName].order === undefined) {
|
||||
MatrixClientPeg.get().setRoomTag(list[i].roomId, self.props.tagName, {order: (order + 1.0) / 2.0}).finally(function() {
|
||||
// Do any final stuff here
|
||||
}).fail(function(err) {
|
||||
}).catch(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to add tag " + self.props.tagName + " to room",
|
||||
description: err.toString()
|
||||
console.error("Failed to add tag " + self.props.tagName + " to room" + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: self.props.tagName}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
@ -506,27 +539,26 @@ var RoomSubList = React.createClass({
|
|||
|
||||
render: function() {
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
var RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
var label = this.props.collapsed ? null : this.props.label;
|
||||
|
||||
//console.log("render: " + JSON.stringify(this.state.sortedList));
|
||||
|
||||
var target;
|
||||
if (this.state.sortedList.length == 0 && this.props.editable) {
|
||||
target = <RoomDropTarget label={ 'Drop here to ' + this.props.verb }/>;
|
||||
let content;
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.editable) {
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
var subList;
|
||||
var classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={ classes } truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile} >
|
||||
{ target }
|
||||
{ this.makeRoomTiles() }
|
||||
{ content }
|
||||
</TruncatedList>;
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -16,10 +16,13 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('matrix-react-sdk')
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc');
|
||||
import React from 'react';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
|
||||
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SearchBox',
|
||||
|
@ -35,6 +38,30 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (this.refs.search && payload.clear_search) {
|
||||
this._clearSearch();
|
||||
}
|
||||
break;
|
||||
case 'focus_room_filter':
|
||||
if (this.refs.search) {
|
||||
this.refs.search.focus();
|
||||
this.refs.search.select();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onChange: function() {
|
||||
if (!this.refs.search) return;
|
||||
this.setState({ searchTerm: this.refs.search.value });
|
||||
|
@ -61,35 +88,51 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
this._clearSearch();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_clearSearch: function() {
|
||||
this.refs.search.value = "";
|
||||
this.onChange();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
var collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
|
||||
|
||||
var toggleCollapse;
|
||||
if (this.props.collapsed) {
|
||||
toggleCollapse =
|
||||
<div className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="<"/>
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBox_maximise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") }/>
|
||||
</AccessibleButton>
|
||||
}
|
||||
else {
|
||||
toggleCollapse =
|
||||
<div className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="<"/>
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBox_minimise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") }/>
|
||||
</AccessibleButton>
|
||||
}
|
||||
|
||||
var searchControls;
|
||||
if (!this.props.collapsed) {
|
||||
searchControls = [
|
||||
this.state.searchTerm.length > 0 ?
|
||||
<div key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ ()=>{ this.refs.search.value = ""; this.onChange(); } }>
|
||||
<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ ()=>{ this._clearSearch(); } }>
|
||||
<TintableSvg
|
||||
className="mx_SearchBox_searchButton"
|
||||
src="img/icons-close.svg" width="24" height="24"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
:
|
||||
<TintableSvg
|
||||
key="button"
|
||||
|
@ -103,7 +146,8 @@ module.exports = React.createClass({
|
|||
className="mx_SearchBox_search"
|
||||
value={ this.state.searchTerm }
|
||||
onChange={ this.onChange }
|
||||
placeholder="Filter room names"
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
|
30
src/components/views/context_menus/GenericTextContextMenu.js
Normal file
30
src/components/views/context_menus/GenericTextContextMenu.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class GenericTextContextMenu extends React.Component {
|
||||
static PropTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div>{ this.props.message }</div>;
|
||||
}
|
||||
}
|
|
@ -16,14 +16,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var Modal = require('matrix-react-sdk/lib/Modal');
|
||||
var Resend = require("matrix-react-sdk/lib/Resend");
|
||||
import * as UserSettingsStore from 'matrix-react-sdk/lib/UserSettingsStore';
|
||||
const MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
const dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
const sdk = require('matrix-react-sdk');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
const Modal = require('matrix-react-sdk/lib/Modal');
|
||||
const Resend = require("matrix-react-sdk/lib/Resend");
|
||||
import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageContextMenu',
|
||||
|
@ -39,48 +40,122 @@ module.exports = React.createClass({
|
|||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
canRedact: false,
|
||||
canPin: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
||||
this._checkPermissions();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
|
||||
}
|
||||
},
|
||||
|
||||
_checkPermissions: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
|
||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
||||
|
||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||
if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false;
|
||||
|
||||
this.setState({canRedact, canPin});
|
||||
},
|
||||
|
||||
_isPinned: function() {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
if (!pinnedEvent) return false;
|
||||
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
|
||||
},
|
||||
|
||||
onResendClick: function() {
|
||||
Resend.resend(this.props.mxEvent);
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewSourceClick: function() {
|
||||
var ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createDialog(ViewSource, {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
content: this.props.mxEvent.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewClearSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createDialog(ViewSource, {
|
||||
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
|
||||
// FIXME: _clearEvent is private
|
||||
content: this.props.mxEvent._clearEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onRedactClick: function() {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
|
||||
).done(function() {
|
||||
// message should disappear by itself
|
||||
}, function(e) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
var code = e.errcode || e.statusCode;
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "You cannot delete this message. (" + code + ")"
|
||||
});
|
||||
});
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
|
||||
).catch(function(e) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
const code = e.errcode || e.statusCode;
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this message. (%(code)s)', {code: code})
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onCancelSendClick: function() {
|
||||
Resend.removeFromQueue(this.props.mxEvent);
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onForwardClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onPinClick: function() {
|
||||
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
|
||||
.catch(e => {
|
||||
// Intercept the Event Not Found error and fall through the promise chain with no event.
|
||||
if (e.errcode === "M_NOT_FOUND") return null;
|
||||
throw e;
|
||||
})
|
||||
.then(event => {
|
||||
const eventIds = (event ? event.pinned : []) || [];
|
||||
if (!eventIds.includes(this.props.mxEvent.getId())) {
|
||||
// Not pinned - add
|
||||
eventIds.push(this.props.mxEvent.getId());
|
||||
} else {
|
||||
// Pinned - remove
|
||||
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
|
@ -91,40 +166,43 @@ module.exports = React.createClass({
|
|||
if (this.props.eventTileOps) {
|
||||
this.props.eventTileOps.unhideWidget();
|
||||
}
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onQuoteClick: function () {
|
||||
console.log(this.props.mxEvent);
|
||||
onQuoteClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
event: this.props.mxEvent,
|
||||
text: this.props.eventTileOps.getInnerText(),
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var eventStatus = this.props.mxEvent.status;
|
||||
var resendButton;
|
||||
var viewSourceButton;
|
||||
var viewClearSourceButton;
|
||||
var redactButton;
|
||||
var cancelButton;
|
||||
var permalinkButton;
|
||||
var unhidePreviewButton;
|
||||
var externalURLButton;
|
||||
const eventStatus = this.props.mxEvent.status;
|
||||
let resendButton;
|
||||
let redactButton;
|
||||
let cancelButton;
|
||||
let forwardButton;
|
||||
let pinButton;
|
||||
let viewSourceButton;
|
||||
let viewClearSourceButton;
|
||||
let unhidePreviewButton;
|
||||
let permalinkButton;
|
||||
let externalURLButton;
|
||||
let quoteButton;
|
||||
|
||||
if (eventStatus === 'not_sent') {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
Resend
|
||||
{ _t('Resend') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventStatus) { // sent
|
||||
if (!eventStatus && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
Redact
|
||||
{ _t('Remove') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -132,21 +210,40 @@ module.exports = React.createClass({
|
|||
if (eventStatus === "queued" || eventStatus === "not_sent") {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
Cancel Sending
|
||||
{ _t('Cancel Sending') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{this._isPinned() ? _t('Unpin Message') : _t('Pin Message')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
View Source
|
||||
{ _t('View Source') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
View Decrypted Source
|
||||
{ _t('View Decrypted Source') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -155,9 +252,9 @@ module.exports = React.createClass({
|
|||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
Unhide Preview
|
||||
{ _t('Unhide Preview') }
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,22 +262,24 @@ module.exports = React.createClass({
|
|||
permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={ "https://matrix.to/#/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }
|
||||
target="_blank" rel="noopener" onClick={ this.closeMenu }>Permalink</a>
|
||||
target="_blank" rel="noopener" onClick={ this.closeMenu }>{ _t('Permalink') }</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const quoteButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
Quote
|
||||
</div>
|
||||
);
|
||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
||||
quoteButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Bridges can provide a 'external_url' to link back to the source.
|
||||
if( typeof(this.props.mxEvent.event.content.external_url) === "string") {
|
||||
externalURLButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={ this.props.mxEvent.event.content.external_url }
|
||||
rel="noopener" target="_blank" onClick={ this.closeMenu }>Source URL</a>
|
||||
rel="noopener" target="_blank" onClick={ this.closeMenu }>{ _t('Source URL') }</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -191,13 +290,15 @@ module.exports = React.createClass({
|
|||
{resendButton}
|
||||
{redactButton}
|
||||
{cancelButton}
|
||||
{forwardButton}
|
||||
{pinButton}
|
||||
{viewSourceButton}
|
||||
{viewClearSourceButton}
|
||||
{unhidePreviewButton}
|
||||
{permalinkButton}
|
||||
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ? quoteButton : null}
|
||||
{quoteButton}
|
||||
{externalURLButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
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';
|
||||
|
||||
var q = require("q");
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs');
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'NotificationStateContextMenu',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_save: function(newState) {
|
||||
const oldState = this.state.roomNotifState;
|
||||
const roomId = this.props.room.roomId;
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
if (cli.isGuest()) return;
|
||||
|
||||
this.setState({
|
||||
roomNotifState: newState,
|
||||
});
|
||||
RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
|
||||
// delay slightly so that the user can see their state change
|
||||
// before closing the menu
|
||||
return q.delay(500).then(() => {
|
||||
if (this._unmounted) return;
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
});
|
||||
}, (error) => {
|
||||
// TODO: some form of error notification to the user
|
||||
// to inform them that their state change failed.
|
||||
// For now we at least set the state back
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
roomNotifState: oldState,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onClickAlertMe: function() {
|
||||
this._save(RoomNotifs.ALL_MESSAGES_LOUD);
|
||||
},
|
||||
|
||||
_onClickAllNotifs: function() {
|
||||
this._save(RoomNotifs.ALL_MESSAGES);
|
||||
},
|
||||
|
||||
_onClickMentions: function() {
|
||||
this._save(RoomNotifs.MENTIONS_ONLY);
|
||||
},
|
||||
|
||||
_onClickMute: function() {
|
||||
this._save(RoomNotifs.MUTE);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var alertMeClasses = classNames({
|
||||
'mx_NotificationStateContextMenu_field': true,
|
||||
'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
|
||||
});
|
||||
|
||||
var allNotifsClasses = classNames({
|
||||
'mx_NotificationStateContextMenu_field': true,
|
||||
'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
|
||||
});
|
||||
|
||||
var mentionsClasses = classNames({
|
||||
'mx_NotificationStateContextMenu_field': true,
|
||||
'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
|
||||
});
|
||||
|
||||
var muteNotifsClasses = classNames({
|
||||
'mx_NotificationStateContextMenu_field': true,
|
||||
'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_NotificationStateContextMenu_picker" >
|
||||
<img src="img/notif-slider.svg" width="20" height="107" />
|
||||
</div>
|
||||
<div className={ alertMeClasses } onClick={this._onClickAlertMe} >
|
||||
<img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
|
||||
All messages (loud)
|
||||
</div>
|
||||
<div className={ allNotifsClasses } onClick={this._onClickAllNotifs} >
|
||||
<img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-off.svg" width="16" height="12" />
|
||||
All messages
|
||||
</div>
|
||||
<div className={ mentionsClasses } onClick={this._onClickMentions} >
|
||||
<img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
|
||||
Mentions only
|
||||
</div>
|
||||
<div className={ muteNotifsClasses } onClick={this._onClickMute} >
|
||||
<img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute.svg" width="16" height="12" />
|
||||
Mute
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
101
src/components/views/context_menus/PresenceContextMenu.js
Normal file
101
src/components/views/context_menus/PresenceContextMenu.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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 { _t, _td } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
|
||||
const STATUS_LABELS = {
|
||||
"online": _td("Online"),
|
||||
"unavailable": _td("Away"),
|
||||
"offline": _td("Appear Offline"),
|
||||
};
|
||||
|
||||
const PresenceContextMenuOption = React.createClass({
|
||||
displayName: 'PresenceContextMenuOption',
|
||||
|
||||
propTypes: {
|
||||
forStatus: React.PropTypes.string.isRequired,
|
||||
isCurrent: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
if (this.isCurrent) return;
|
||||
this.props.onChange(this.props.forStatus);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const indicatorClasses = "mx_PresenceContextMenuOption_indicator "
|
||||
+ "mx_PresenceContextMenuOption_indicator_" + this.props.forStatus;
|
||||
|
||||
let classNames = "mx_PresenceContextMenuOption";
|
||||
if (this.props.isCurrent) classNames += " mx_PresenceContextMenuOption_current";
|
||||
|
||||
return (
|
||||
<AccessibleButton className={classNames} element="div" onClick={this.onClick}>
|
||||
<div className={indicatorClasses}></div>
|
||||
{ _t(STATUS_LABELS[this.props.forStatus]) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PresenceContextMenu',
|
||||
|
||||
propTypes: {
|
||||
// "online", "unavailable", or "offline"
|
||||
currentStatus: React.PropTypes.string.isRequired,
|
||||
|
||||
// Called when the user wants to change their status.
|
||||
// Args: (newStatus:string)
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the menu is dismissed
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
currentStatus: this.props.currentStatus,
|
||||
};
|
||||
},
|
||||
|
||||
onChange: function(newStatus) {
|
||||
this.props.onChange(newStatus);
|
||||
this.setState({currentStatus: newStatus});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const statusElements = [];
|
||||
for (let status of Object.keys(STATUS_LABELS)) {
|
||||
statusElements.push((
|
||||
<PresenceContextMenuOption forStatus={status} key={status}
|
||||
onChange={this.onChange}
|
||||
isCurrent={status === this.state.currentStatus} />
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ statusElements }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,253 +0,0 @@
|
|||
/*
|
||||
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';
|
||||
|
||||
var q = require("q");
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
var DMRoomMap = require('matrix-react-sdk/lib/utils/DMRoomMap');
|
||||
var Rooms = require('matrix-react-sdk/lib/Rooms');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTagContextMenu',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
return {
|
||||
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
|
||||
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
|
||||
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
|
||||
};
|
||||
},
|
||||
|
||||
_toggleTag: function(tagNameOn, tagNameOff) {
|
||||
var self = this;
|
||||
const roomId = this.props.room.roomId;
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (!cli.isGuest()) {
|
||||
q.delay(500).then(function() {
|
||||
if (tagNameOff !== null && tagNameOff !== undefined) {
|
||||
cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
|
||||
// Close the context menu
|
||||
if (self.props.onFinished) {
|
||||
self.props.onFinished();
|
||||
};
|
||||
}).fail(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to remove tag " + tagNameOff + " from room",
|
||||
description: err.toString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (tagNameOn !== null && tagNameOn !== undefined) {
|
||||
// If the tag ordering meta data is required, it is added by
|
||||
// the RoomSubList when it sorts its rooms
|
||||
cli.setRoomTag(roomId, tagNameOn, {}).finally(function() {
|
||||
// Close the context menu
|
||||
if (self.props.onFinished) {
|
||||
self.props.onFinished();
|
||||
};
|
||||
}).fail(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to add tag " + tagNameOn + " to room",
|
||||
description: err.toString()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onClickFavourite: function() {
|
||||
// Tag room as 'Favourite'
|
||||
if (!this.state.isFavourite && this.state.isLowPriority) {
|
||||
this.setState({
|
||||
isFavourite: true,
|
||||
isLowPriority: false,
|
||||
});
|
||||
this._toggleTag("m.favourite", "m.lowpriority");
|
||||
} else if (this.state.isFavourite) {
|
||||
this.setState({isFavourite: false});
|
||||
this._toggleTag(null, "m.favourite");
|
||||
} else if (!this.state.isFavourite) {
|
||||
this.setState({isFavourite: true});
|
||||
this._toggleTag("m.favourite");
|
||||
}
|
||||
},
|
||||
|
||||
_onClickLowPriority: function() {
|
||||
// Tag room as 'Low Priority'
|
||||
if (!this.state.isLowPriority && this.state.isFavourite) {
|
||||
this.setState({
|
||||
isFavourite: false,
|
||||
isLowPriority: true,
|
||||
});
|
||||
this._toggleTag("m.lowpriority", "m.favourite");
|
||||
} else if (this.state.isLowPriority) {
|
||||
this.setState({isLowPriority: false});
|
||||
this._toggleTag(null, "m.lowpriority");
|
||||
} else if (!this.state.isLowPriority) {
|
||||
this.setState({isLowPriority: true});
|
||||
this._toggleTag("m.lowpriority");
|
||||
}
|
||||
},
|
||||
|
||||
_onClickDM: function() {
|
||||
const newIsDirectMessage = !this.state.isDirectMessage;
|
||||
this.setState({
|
||||
isDirectMessage: newIsDirectMessage,
|
||||
});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
let newTarget;
|
||||
if (newIsDirectMessage) {
|
||||
const guessedTarget = Rooms.guessDMRoomTarget(
|
||||
this.props.room,
|
||||
this.props.room.getMember(MatrixClientPeg.get().credentials.userId),
|
||||
);
|
||||
newTarget = guessedTarget.userId;
|
||||
} else {
|
||||
newTarget = null;
|
||||
}
|
||||
|
||||
// give some time for the user to see the icon change first, since
|
||||
// this will hide the context menu once it completes
|
||||
q.delay(500).done(() => {
|
||||
return Rooms.setDMRoom(this.props.room.roomId, newTarget).finally(() => {
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
}, (err) => {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set Direct Message status of room",
|
||||
description: err.toString()
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onClickLeave: function() {
|
||||
// Leave room
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
},
|
||||
|
||||
_onClickForget: function() {
|
||||
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
|
||||
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}, function(err) {
|
||||
var errCode = err.errcode || "unknown error code";
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: `Failed to forget room (${errCode})`
|
||||
});
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myMember = this.props.room.getMember(myUserId);
|
||||
|
||||
const favouriteClasses = classNames({
|
||||
'mx_RoomTagContextMenu_field': true,
|
||||
'mx_RoomTagContextMenu_fieldSet': this.state.isFavourite,
|
||||
'mx_RoomTagContextMenu_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const lowPriorityClasses = classNames({
|
||||
'mx_RoomTagContextMenu_field': true,
|
||||
'mx_RoomTagContextMenu_fieldSet': this.state.isLowPriority,
|
||||
'mx_RoomTagContextMenu_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const leaveClasses = classNames({
|
||||
'mx_RoomTagContextMenu_field': true,
|
||||
'mx_RoomTagContextMenu_fieldSet': false,
|
||||
'mx_RoomTagContextMenu_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const dmClasses = classNames({
|
||||
'mx_RoomTagContextMenu_field': true,
|
||||
'mx_RoomTagContextMenu_fieldSet': this.state.isDirectMessage,
|
||||
'mx_RoomTagContextMenu_fieldDisabled': false,
|
||||
});
|
||||
|
||||
if (myMember && myMember.membership === "leave") {
|
||||
return (
|
||||
<div>
|
||||
<div className={ leaveClasses } onClick={ this._onClickForget } >
|
||||
<img className="mx_RoomTagContextMenu_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
Forget
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={ favouriteClasses } onClick={this._onClickFavourite} >
|
||||
<img className="mx_RoomTagContextMenu_icon" src="img/icon_context_fave.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_fave_on.svg" width="15" height="15" />
|
||||
Favourite
|
||||
</div>
|
||||
<div className={ lowPriorityClasses } onClick={this._onClickLowPriority} >
|
||||
<img className="mx_RoomTagContextMenu_icon" src="img/icon_context_low.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_low_on.svg" width="15" height="15" />
|
||||
Low Priority
|
||||
</div>
|
||||
<div className={ dmClasses } onClick={this._onClickDM} >
|
||||
<img className="mx_RoomTagContextMenu_icon" src="img/icon_context_person.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_person_on.svg" width="15" height="15" />
|
||||
Direct Chat
|
||||
</div>
|
||||
<hr className="mx_RoomTagContextMenu_separator" />
|
||||
<div className={ leaveClasses } onClick={(myMember && myMember.membership === "join") ? this._onClickLeave : null} >
|
||||
<img className="mx_RoomTagContextMenu_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
Leave
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
393
src/components/views/context_menus/RoomTileContextMenu.js
Normal file
393
src/components/views/context_menus/RoomTileContextMenu.js
Normal file
|
@ -0,0 +1,393 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import { _t, _td } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import DMRoomMap from 'matrix-react-sdk/lib/utils/DMRoomMap';
|
||||
import * as Rooms from 'matrix-react-sdk/lib/Rooms';
|
||||
import * as RoomNotifs from 'matrix-react-sdk/lib/RoomNotifs';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTileContextMenu',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
return {
|
||||
roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
|
||||
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
|
||||
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_toggleTag: function(tagNameOn, tagNameOff) {
|
||||
var self = this;
|
||||
const roomId = this.props.room.roomId;
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (!cli.isGuest()) {
|
||||
Promise.delay(500).then(function() {
|
||||
if (tagNameOff !== null && tagNameOff !== undefined) {
|
||||
cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
|
||||
// Close the context menu
|
||||
if (self.props.onFinished) {
|
||||
self.props.onFinished();
|
||||
};
|
||||
}).catch(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove tag from room 1', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOff}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (tagNameOn !== null && tagNameOn !== undefined) {
|
||||
// If the tag ordering meta data is required, it is added by
|
||||
// the RoomSubList when it sorts its rooms
|
||||
cli.setRoomTag(roomId, tagNameOn, {}).finally(function() {
|
||||
// Close the context menu
|
||||
if (self.props.onFinished) {
|
||||
self.props.onFinished();
|
||||
};
|
||||
}).catch(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove tag from room 2', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOn}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onClickFavourite: function() {
|
||||
// Tag room as 'Favourite'
|
||||
if (!this.state.isFavourite && this.state.isLowPriority) {
|
||||
this.setState({
|
||||
isFavourite: true,
|
||||
isLowPriority: false,
|
||||
});
|
||||
this._toggleTag("m.favourite", "m.lowpriority");
|
||||
} else if (this.state.isFavourite) {
|
||||
this.setState({isFavourite: false});
|
||||
this._toggleTag(null, "m.favourite");
|
||||
} else if (!this.state.isFavourite) {
|
||||
this.setState({isFavourite: true});
|
||||
this._toggleTag("m.favourite");
|
||||
}
|
||||
},
|
||||
|
||||
_onClickLowPriority: function() {
|
||||
// Tag room as 'Low Priority'
|
||||
if (!this.state.isLowPriority && this.state.isFavourite) {
|
||||
this.setState({
|
||||
isFavourite: false,
|
||||
isLowPriority: true,
|
||||
});
|
||||
this._toggleTag("m.lowpriority", "m.favourite");
|
||||
} else if (this.state.isLowPriority) {
|
||||
this.setState({isLowPriority: false});
|
||||
this._toggleTag(null, "m.lowpriority");
|
||||
} else if (!this.state.isLowPriority) {
|
||||
this.setState({isLowPriority: true});
|
||||
this._toggleTag("m.lowpriority");
|
||||
}
|
||||
},
|
||||
|
||||
_onClickDM: function() {
|
||||
const newIsDirectMessage = !this.state.isDirectMessage;
|
||||
this.setState({
|
||||
isDirectMessage: newIsDirectMessage,
|
||||
});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
Rooms.guessAndSetDMRoom(
|
||||
this.props.room, newIsDirectMessage
|
||||
).delay(500).finally(() => {
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
}, (err) => {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, {
|
||||
title: _t('Failed to set Direct Message status of room'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onClickLeave: function() {
|
||||
// Leave room
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
},
|
||||
|
||||
_onClickReject: function() {
|
||||
dis.dispatch({
|
||||
action: 'reject_invite',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
},
|
||||
|
||||
_onClickForget: function() {
|
||||
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
|
||||
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}, function(err) {
|
||||
var errCode = err.errcode || _td("unknown error code");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
|
||||
title: _t('Failed to forget room %(errCode)s', {errCode: errCode}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
},
|
||||
|
||||
_saveNotifState: function(newState) {
|
||||
const oldState = this.state.roomNotifState;
|
||||
const roomId = this.props.room.roomId;
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
if (cli.isGuest()) return;
|
||||
|
||||
this.setState({
|
||||
roomNotifState: newState,
|
||||
});
|
||||
RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
|
||||
// delay slightly so that the user can see their state change
|
||||
// before closing the menu
|
||||
return Promise.delay(500).then(() => {
|
||||
if (this._unmounted) return;
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
};
|
||||
});
|
||||
}, (error) => {
|
||||
// TODO: some form of error notification to the user
|
||||
// to inform them that their state change failed.
|
||||
// For now we at least set the state back
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
roomNotifState: oldState,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onClickAlertMe: function() {
|
||||
this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD);
|
||||
},
|
||||
|
||||
_onClickAllNotifs: function() {
|
||||
this._saveNotifState(RoomNotifs.ALL_MESSAGES);
|
||||
},
|
||||
|
||||
_onClickMentions: function() {
|
||||
this._saveNotifState(RoomNotifs.MENTIONS_ONLY);
|
||||
},
|
||||
|
||||
_onClickMute: function() {
|
||||
this._saveNotifState(RoomNotifs.MUTE);
|
||||
},
|
||||
|
||||
_renderNotifMenu: function() {
|
||||
var alertMeClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
|
||||
});
|
||||
|
||||
var allNotifsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
|
||||
});
|
||||
|
||||
var mentionsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
|
||||
});
|
||||
|
||||
var muteNotifsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_RoomTileContextMenu_notif_picker" >
|
||||
<img src="img/notif-slider.svg" width="20" height="107" />
|
||||
</div>
|
||||
<div className={ alertMeClasses } onClick={this._onClickAlertMe} >
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
|
||||
{ _t('All messages (loud)') }
|
||||
</div>
|
||||
<div className={ allNotifsClasses } onClick={this._onClickAllNotifs} >
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-off.svg" width="16" height="12" />
|
||||
{ _t('All messages') }
|
||||
</div>
|
||||
<div className={ mentionsClasses } onClick={this._onClickMentions} >
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
|
||||
{ _t('Mentions only') }
|
||||
</div>
|
||||
<div className={ muteNotifsClasses } onClick={this._onClickMute} >
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute.svg" width="16" height="12" />
|
||||
{ _t('Mute') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLeaveMenu: function(membership) {
|
||||
if (!membership) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let leaveClickHandler = null;
|
||||
let leaveText = null;
|
||||
|
||||
switch (membership) {
|
||||
case "join":
|
||||
leaveClickHandler = this._onClickLeave;
|
||||
leaveText = _t('Leave');
|
||||
break;
|
||||
case "leave":
|
||||
case "ban":
|
||||
leaveClickHandler = this._onClickForget;
|
||||
leaveText = _t('Forget');
|
||||
break;
|
||||
case "invite":
|
||||
leaveClickHandler = this._onClickReject;
|
||||
leaveText = _t('Reject');
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={ leaveClickHandler } >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ leaveText }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderRoomTagMenu: function() {
|
||||
const favouriteClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const lowPriorityClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isLowPriority,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const dmClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isDirectMessage,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={ favouriteClasses } onClick={this._onClickFavourite} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_fave.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_fave_on.svg" width="15" height="15" />
|
||||
{ _t('Favourite') }
|
||||
</div>
|
||||
<div className={ lowPriorityClasses } onClick={this._onClickLowPriority} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_low.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_low_on.svg" width="15" height="15" />
|
||||
{ _t('Low Priority') }
|
||||
</div>
|
||||
<div className={ dmClasses } onClick={this._onClickDM} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_person.svg" width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_person_on.svg" width="15" height="15" />
|
||||
{ _t('Direct Chat') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const myMember = this.props.room.getMember(
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
);
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMember.membership !== 'join') {
|
||||
return this._renderLeaveMenu(myMember.membership);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMember.membership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
176
src/components/views/dialogs/BugReportDialog.js
Normal file
176
src/components/views/dialogs/BugReportDialog.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import SdkConfig from 'matrix-react-sdk/lib/SdkConfig';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
export default class BugReportDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
sendLogs: true,
|
||||
busy: false,
|
||||
err: null,
|
||||
text: "",
|
||||
progress: null,
|
||||
};
|
||||
this._unmounted = false;
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onTextChange = this._onTextChange.bind(this);
|
||||
this._onSendLogsChange = this._onSendLogsChange.bind(this);
|
||||
this._sendProgressCallback = this._sendProgressCallback.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onCancel(ev) {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onSubmit(ev) {
|
||||
const sendLogs = this.state.sendLogs;
|
||||
const userText = this.state.text;
|
||||
if (!sendLogs && userText.trim().length === 0) {
|
||||
this.setState({
|
||||
err: _t("Please describe the bug and/or send logs."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({ busy: true, progress: null, err: null });
|
||||
this._sendProgressCallback(_t("Loading bug report module"));
|
||||
|
||||
require(['../../../vector/submit-rageshake'], (s) => {
|
||||
s(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText: userText,
|
||||
sendLogs: sendLogs,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
|
||||
title: _t('Bug report sent'),
|
||||
description: _t('Thank you!'),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
err: _t("Failed to send report: ") + `${err.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onTextChange(ev) {
|
||||
this.setState({ text: ev.target.value });
|
||||
}
|
||||
|
||||
_onSendLogsChange(ev) {
|
||||
this.setState({ sendLogs: ev.target.checked });
|
||||
}
|
||||
|
||||
_sendProgressCallback(progress) {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({progress: progress});
|
||||
}
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
{this.state.progress} ...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_BugReportDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{ _t("Report a bug") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t("Please describe the bug. What did you do? What did you expect to happen? What actually happened?") }
|
||||
</p>
|
||||
<textarea
|
||||
className="mx_BugReportDialog_input"
|
||||
rows={5}
|
||||
onChange={this._onTextChange}
|
||||
value={this.state.text}
|
||||
placeholder={_t("Describe your problem here.")}
|
||||
/>
|
||||
<p>
|
||||
{ _t("In order to diagnose problems, logs from this client will be sent with this bug report. If you would prefer to only send the text above, please untick:") }
|
||||
</p>
|
||||
<input type="checkbox" checked={this.state.sendLogs}
|
||||
onChange={this._onSendLogsChange} id="mx_BugReportDialog_logs"/>
|
||||
<label htmlFor="mx_BugReportDialog_logs">
|
||||
{ _t("Send logs") }
|
||||
</label>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary danger"
|
||||
onClick={this._onSubmit}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{ _t("Send") }
|
||||
</button>
|
||||
|
||||
{cancelButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
|
@ -17,6 +17,7 @@
|
|||
import React from 'react';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import request from 'browser-request';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
const REPOS = ['vector-im/vector-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
||||
|
||||
|
@ -70,16 +71,16 @@ export default class ChangelogDialog extends React.Component {
|
|||
|
||||
const content = (
|
||||
<div className="mx_ChangelogDialog_content">
|
||||
{this.props.version == null || this.props.newVersion == null ? <h2>Unavailable</h2> : logs}
|
||||
{this.props.version == null || this.props.newVersion == null ? <h2>{_t("Unavailable")}</h2> : logs}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title="Changelog"
|
||||
title={_t("Changelog")}
|
||||
description={content}
|
||||
button="Update"
|
||||
button={_t("Update")}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
)
|
||||
|
|
594
src/components/views/dialogs/DevtoolsDialog.js
Normal file
594
src/components/views/dialogs/DevtoolsDialog.js
Normal file
|
@ -0,0 +1,594 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 'matrix-react-sdk';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||
|
||||
class DevtoolsComponent extends React.Component {
|
||||
static contextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
class GenericEditor extends DevtoolsComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.state.message) {
|
||||
this.setState({ message: null });
|
||||
} else {
|
||||
this.props.onBack();
|
||||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
_buttons() {
|
||||
return <div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
</div>;
|
||||
}
|
||||
|
||||
textInput(id, label) {
|
||||
return <div className="mx_DevTools_inputRow">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor={id}>{ label }</label>
|
||||
</div>
|
||||
<div className="mx_DevTools_inputCell">
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class SendCustomEvent extends GenericEditor {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, stateKey, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
stateKey: '',
|
||||
evContent: '{\n\n}',
|
||||
}, this.props.inputs);
|
||||
|
||||
this.state = {
|
||||
isStateEvent: Boolean(this.props.forceStateEvent),
|
||||
|
||||
eventType,
|
||||
stateKey,
|
||||
evContent,
|
||||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (this.state.isStateEvent) {
|
||||
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
} else {
|
||||
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
|
||||
}
|
||||
}
|
||||
|
||||
async _send() {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
}
|
||||
|
||||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
}
|
||||
this.setState({ message });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.message) {
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class SendAccountData extends GenericEditor {
|
||||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
isRoomAccountData: PropTypes.bool,
|
||||
forceMode: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
evContent: '{\n\n}',
|
||||
}, this.props.inputs);
|
||||
|
||||
this.state = {
|
||||
isRoomAccountData: Boolean(this.props.isRoomAccountData),
|
||||
|
||||
eventType,
|
||||
evContent,
|
||||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
|
||||
}
|
||||
return cli.setAccountData(this.state.eventType, content);
|
||||
}
|
||||
|
||||
async _send() {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
}
|
||||
|
||||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
}
|
||||
this.setState({ message });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.message) {
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onQuery = this.onQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
};
|
||||
}
|
||||
|
||||
onQuery(ev) {
|
||||
this.setState({ query: ev.target.value });
|
||||
}
|
||||
|
||||
filterChildren() {
|
||||
if (this.state.query) {
|
||||
const lowerQuery = this.state.query.toLowerCase();
|
||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<input size="64"
|
||||
onChange={this.onQuery}
|
||||
value={this.state.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends DevtoolsComponent {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
||||
this.roomStateEvents = room.currentState.events;
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
|
||||
this.state = {
|
||||
eventType: null,
|
||||
event: null,
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
|
||||
browseEventType(eventType) {
|
||||
return () => {
|
||||
this.setState({ eventType });
|
||||
};
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
this.setState({ event: null });
|
||||
} else if (this.state.eventType) {
|
||||
this.setState({ eventType: null });
|
||||
} else {
|
||||
this.props.onBack();
|
||||
}
|
||||
}
|
||||
|
||||
editEv() {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
}} />;
|
||||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
<div className="mx_Dialog_content">
|
||||
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
<button onClick={this.editEv}>{ _t('Edit') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
const classes = 'mx_DevTools_RoomStateExplorer_button';
|
||||
if (this.state.eventType === null) {
|
||||
Object.keys(this.roomStateEvents).forEach((evType) => {
|
||||
const stateGroup = this.roomStateEvents[evType];
|
||||
const stateKeys = Object.keys(stateGroup);
|
||||
|
||||
let onClickFn;
|
||||
if (stateKeys.length > 1) {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
} else if (stateKeys.length === 1) {
|
||||
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
|
||||
}
|
||||
|
||||
rows.push(<button className={classes} key={evType} onClick={onClickFn}>
|
||||
{ evType }
|
||||
</button>);
|
||||
});
|
||||
} else {
|
||||
const evType = this.state.eventType;
|
||||
const stateGroup = this.roomStateEvents[evType];
|
||||
Object.keys(stateGroup).forEach((stateKey) => {
|
||||
const ev = stateGroup[stateKey];
|
||||
rows.push(<button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
|
||||
{ stateKey }
|
||||
</button>);
|
||||
});
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<FilteredList>
|
||||
{ rows }
|
||||
</FilteredList>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends DevtoolsComponent {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
isRoomAccountData: false,
|
||||
event: null,
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
|
||||
getData() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.getRoom(this.context.roomId).accountData;
|
||||
}
|
||||
return cli.store.accountData;
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
this.setState({ event: null });
|
||||
} else {
|
||||
this.props.onBack();
|
||||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
editEv() {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
}} forceMode={true} />;
|
||||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
<div className="mx_Dialog_content">
|
||||
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
<button onClick={this.editEv}>{ _t('Edit') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
const classes = 'mx_DevTools_RoomStateExplorer_button';
|
||||
|
||||
const data = this.getData();
|
||||
Object.keys(data).forEach((evType) => {
|
||||
const ev = data[evType];
|
||||
rows.push(<button className={classes} key={evType} onClick={this.onViewSourceClick(ev)}>
|
||||
{ evType }
|
||||
</button>);
|
||||
});
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<FilteredList>
|
||||
{ rows }
|
||||
</FilteredList>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
AccountDataExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.Component {
|
||||
static childContextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
// client: PropTypes.instanceOf(MatixClient),
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
mode: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return { roomId: this.props.roomId };
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
return () => {
|
||||
this.setState({ mode });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.prevMode) {
|
||||
this.setState({ mode: this.prevMode });
|
||||
this.prevMode = null;
|
||||
} else {
|
||||
this.setState({ mode: null });
|
||||
}
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let body;
|
||||
|
||||
if (this.state.mode) {
|
||||
body = <div>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} />
|
||||
</div>;
|
||||
} else {
|
||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||
body = <div>
|
||||
<div>
|
||||
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
|
||||
<div className="mx_Dialog_content">
|
||||
{ Entries.map((Entry) => {
|
||||
const label = Entry.getLabel();
|
||||
const onClick = this._setMode(Entry);
|
||||
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}>
|
||||
{ body }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
137
src/components/views/dialogs/SetPasswordDialog.js
Normal file
137
src/components/views/dialogs/SetPasswordDialog.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
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 sdk from 'matrix-react-sdk';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
|
||||
const WarmFuzzy = function(props) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
let title = _t('You have successfully set a password!');
|
||||
if (props.didSetEmail) {
|
||||
title = _t('You have successfully set a password and an email address!');
|
||||
}
|
||||
const advice = _t('You can now return to your account after signing out, and sign in on other devices.');
|
||||
let extraAdvice = null;
|
||||
if (!props.didSetEmail) {
|
||||
extraAdvice = _t('Remember, you can always set an email address in user settings if you change your mind.');
|
||||
}
|
||||
|
||||
return <BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={props.onFinished}
|
||||
title={ title }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ advice }
|
||||
</p>
|
||||
<p>
|
||||
{ extraAdvice }
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary"
|
||||
autoFocus={true}
|
||||
onClick={props.onFinished}>
|
||||
{ _t('Continue') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt the user to set a password
|
||||
*
|
||||
* On success, `onFinished()` when finished
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'SetPasswordDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
console.info('SetPasswordDialog component will mount');
|
||||
},
|
||||
|
||||
_onPasswordChanged: function(res) {
|
||||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this._onContinueClicked();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onContinueClicked: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onPasswordChangeError: function(err) {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
errMsg = _t('Failed to change password. Is your password correct?');
|
||||
} else if (err.httpStatus) {
|
||||
errMsg += ' ' + _t(
|
||||
'(HTTP status %(httpStatus)s)',
|
||||
{ httpStatus: err.httpStatus },
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
error: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const ChangePassword = sdk.getComponent('views.settings.ChangePassword');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={ _t('Please set a password!') }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t('This will allow you to return to your account after signing out, and sign in on other devices.') }
|
||||
</p>
|
||||
<ChangePassword
|
||||
className="mx_SetPasswordDialog_change_password"
|
||||
rowClassName=""
|
||||
rowLabelClassName=""
|
||||
rowInputClassName=""
|
||||
buttonClassName="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
|
||||
confirm={false}
|
||||
autoFocusNewPasswordInput={true}
|
||||
shouldAskForEmail={true}
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged} />
|
||||
<div className="error">
|
||||
{ this.state.error }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -150,7 +150,21 @@ export default class NetworkDropdown extends React.Component {
|
|||
if (this.props.protocols) {
|
||||
for (const proto of Object.keys(this.props.protocols)) {
|
||||
if (!this.props.protocols[proto].instances) continue;
|
||||
for (const instance of this.props.protocols[proto].instances) {
|
||||
|
||||
const sortedInstances = this.props.protocols[proto].instances;
|
||||
sortedInstances.sort(function(x, y) {
|
||||
const a = x.desc
|
||||
const b = y.desc
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a > b) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (const instance of sortedInstances) {
|
||||
if (!instance.instance_id) continue;
|
||||
options.push(this._makeMenuOption(server, instance, false));
|
||||
}
|
||||
|
@ -181,7 +195,10 @@ export default class NetworkDropdown extends React.Component {
|
|||
span_class = 'mx_NetworkDropdown_menu_network';
|
||||
} else {
|
||||
key = server + '_inst_' + instance.instance_id;
|
||||
icon = <img src={instance.icon || DEFAULT_ICON_URL} />;
|
||||
const imgUrl = instance.icon ?
|
||||
MatrixClientPeg.get().mxcUrlToHttp(instance.icon, 25, 25, 'crop', true) :
|
||||
DEFAULT_ICON_URL;
|
||||
icon = <img src={imgUrl} />;
|
||||
name = instance.desc;
|
||||
span_class = 'mx_NetworkDropdown_menu_network';
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
|||
|
||||
var DateUtils = require('matrix-react-sdk/lib/DateUtils');
|
||||
var filesize = require('filesize');
|
||||
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
|
||||
const Modal = require('matrix-react-sdk/lib/Modal');
|
||||
const sdk = require('matrix-react-sdk');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ImageView',
|
||||
|
@ -61,19 +65,23 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRedactClick: function() {
|
||||
var self = this;
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
|
||||
).done(function() {
|
||||
if (self.props.onFinished) self.props.onFinished();
|
||||
}, function(e) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
var code = e.errcode || e.statusCode;
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "You cannot delete this image. (" + code + ")"
|
||||
});
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
var self = this;
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
|
||||
).catch(function(e) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
var code = e.errcode || e.statusCode;
|
||||
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this image. (%(code)s)', {code: code})
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -142,15 +150,23 @@ module.exports = React.createClass({
|
|||
|
||||
var eventMeta;
|
||||
if(showEventMeta) {
|
||||
// Figure out the sender, defaulting to mxid
|
||||
let sender = this.props.mxEvent.getSender();
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
if (room) {
|
||||
const member = room.getMember(sender);
|
||||
if (member) sender = member.name;
|
||||
}
|
||||
|
||||
eventMeta = (<div className="mx_ImageView_metadata">
|
||||
Uploaded on { DateUtils.formatDate(new Date(this.props.mxEvent.getTs())) } by { this.props.mxEvent.getSender() }
|
||||
{ _t('Uploaded on %(date)s by %(user)s', {date: DateUtils.formatDate(new Date(this.props.mxEvent.getTs())), user: sender}) }
|
||||
</div>);
|
||||
}
|
||||
|
||||
var eventRedact;
|
||||
if(showEventMeta) {
|
||||
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
|
||||
Redact
|
||||
{ _t('Remove') }
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
@ -162,16 +178,16 @@ module.exports = React.createClass({
|
|||
<img src={this.props.src} style={style}/>
|
||||
<div className="mx_ImageView_labelWrapper">
|
||||
<div className="mx_ImageView_label">
|
||||
<img className="mx_ImageView_cancel" src="img/cancel-white.svg" width="18" height="18" alt="Close" onClick={ this.props.onFinished }/>
|
||||
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') }/></AccessibleButton>
|
||||
<div className="mx_ImageView_shim">
|
||||
</div>
|
||||
<div className="mx_ImageView_name">
|
||||
{ this.getName() }
|
||||
</div>
|
||||
{ eventMeta }
|
||||
<a className="mx_ImageView_link" href={ this.props.src } target="_blank" rel="noopener">
|
||||
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
|
||||
<div className="mx_ImageView_download">
|
||||
Download this file<br/>
|
||||
{ _t('Download this file') }<br/>
|
||||
<span className="mx_ImageView_size">{ size_res }</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
33
src/components/views/elements/InlineSpinner.js
Normal file
33
src/components/views/elements/InlineSpinner.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'InlineSpinner',
|
||||
|
||||
render: function() {
|
||||
var w = this.props.w || 16;
|
||||
var h = this.props.h || 16;
|
||||
var imgClass = this.props.imgClassName || "";
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher')
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GuestWarningBar',
|
||||
|
||||
onRegisterClicked: function() {
|
||||
dis.dispatch({'action': 'start_upgrade_registration'});
|
||||
},
|
||||
|
||||
onLoginClicked: function() {
|
||||
dis.dispatch({'action': 'logout'});
|
||||
dis.dispatch({'action': 'start_login'});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_GuestWarningBar">
|
||||
<img className="mx_GuestWarningBar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
|
||||
<div>
|
||||
You are Rioting as a guest. <a onClick={this.onRegisterClicked}>Register</a> or <a onClick={this.onLoginClicked}>sign in</a> to access more rooms and features.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Notifier = require("matrix-react-sdk/lib/Notifier");
|
||||
var sdk = require('matrix-react-sdk')
|
||||
import React from 'react';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import Notifier from 'matrix-react-sdk/lib/Notifier';
|
||||
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MatrixToolbar',
|
||||
|
@ -34,12 +35,12 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
You are not receiving desktop notifications. <a className="mx_MatrixToolbar_link" onClick={ this.onClick }>Enable them now</a>
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
<div className="mx_MatrixToolbar_close"><img src="img/cancel.svg" width="18" height="18" onClick={ this.hideToolbar } /></div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from 'react';
|
|||
import sdk from 'matrix-react-sdk';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
/**
|
||||
* Check a version string is compatible with the Changelog
|
||||
|
@ -39,10 +40,10 @@ export default React.createClass({
|
|||
|
||||
displayReleaseNotes: function(releaseNotes) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "What's New",
|
||||
description: <pre className="changelog_text">{releaseNotes}</pre>,
|
||||
button: "Update",
|
||||
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
|
||||
title: _t("What's New"),
|
||||
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
|
||||
button: _t("Update"),
|
||||
onFinished: (update) => {
|
||||
if(update && PlatformPeg.get()) {
|
||||
PlatformPeg.get().installUpdate();
|
||||
|
@ -53,7 +54,7 @@ export default React.createClass({
|
|||
|
||||
displayChangelog: function() {
|
||||
const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
|
||||
Modal.createDialog(ChangelogDialog, {
|
||||
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
|
||||
version: this.props.version,
|
||||
newVersion: this.props.newVersion,
|
||||
onFinished: (update) => {
|
||||
|
@ -75,17 +76,29 @@ export default React.createClass({
|
|||
// automatically tells you what's changed (provided the versions
|
||||
// are in the right format)
|
||||
if (this.props.releaseNotes) {
|
||||
action_button = <button className="mx_MatrixToolbar_action" onClick={this.displayReleaseNotes}>What's new?</button>;
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.displayReleaseNotes}>
|
||||
{ _t("What's new?") }
|
||||
</button>
|
||||
);
|
||||
} else if (checkVersion(this.props.version) && checkVersion(this.props.newVersion)) {
|
||||
action_button = <button className="mx_MatrixToolbar_action" onClick={this.displayChangelog}>What's new?</button>;
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.displayChangelog}>
|
||||
{ _t("What's new?") }
|
||||
</button>
|
||||
);
|
||||
} else if (PlatformPeg.get()) {
|
||||
action_button = <button className="mx_MatrixToolbar_action" onClick={this.onUpdateClicked}>Update</button>;
|
||||
action_button = (
|
||||
<button className="mx_MatrixToolbar_action" onClick={this.onUpdateClicked}>
|
||||
{ _t("Update") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
A new version of Riot is available.
|
||||
{_t("A new version of Riot is available.")}
|
||||
</div>
|
||||
{action_button}
|
||||
</div>
|
||||
|
|
64
src/components/views/globals/PasswordNagBar.js
Normal file
64
src/components/views/globals/PasswordNagBar.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
onUpdateClicked: function() {
|
||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, {
|
||||
onFinished: (passwordChanged) => {
|
||||
if (!passwordChanged) {
|
||||
return;
|
||||
}
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({
|
||||
action: 'password_changed',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable";
|
||||
return (
|
||||
<div className={toolbarClasses} onClick={this.onUpdateClicked}>
|
||||
<img className="mx_MatrixToolbar_warning"
|
||||
src="img/warning.svg"
|
||||
width="24"
|
||||
height="23"
|
||||
alt="Warning"
|
||||
/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t(
|
||||
"To return to your account in future you need to <u>set a password</u>",
|
||||
{},
|
||||
{ 'u': (sub) => <u>{ sub }</u> },
|
||||
) }
|
||||
</div>
|
||||
<button className="mx_MatrixToolbar_action">
|
||||
{ _t("Set Password") }
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
85
src/components/views/globals/UpdateCheckBar.js
Normal file
85
src/components/views/globals/UpdateCheckBar.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
|
||||
import {updateCheckStatusEnum} from '../../../vector/platform/VectorBasePlatform';
|
||||
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
|
||||
|
||||
const doneStatuses = [
|
||||
updateCheckStatusEnum.ERROR,
|
||||
updateCheckStatusEnum.NOTAVAILABLE,
|
||||
];
|
||||
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
status: React.PropTypes.oneOf(Object.values(updateCheckStatusEnum)).isRequired,
|
||||
// Currently for error detail but will be usable for download progress
|
||||
// once that is a thing that squirrel passes through electron.
|
||||
detail: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
detail: '',
|
||||
}
|
||||
},
|
||||
|
||||
getStatusText: function() {
|
||||
switch(this.props.status) {
|
||||
case updateCheckStatusEnum.ERROR:
|
||||
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
|
||||
case updateCheckStatusEnum.CHECKING:
|
||||
return _t('Checking for an update...');
|
||||
case updateCheckStatusEnum.NOTAVAILABLE:
|
||||
return _t('No update available.');
|
||||
case updateCheckStatusEnum.DOWNLOADING:
|
||||
return _t('Downloading update...');
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
hideToolbar: function() {
|
||||
PlatformPeg.get().stopUpdateCheck();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const message = this.getStatusText();
|
||||
const warning = _t('Warning');
|
||||
|
||||
let image;
|
||||
if (doneStatuses.includes(this.props.status)) {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt={warning}/>;
|
||||
} else {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt={message}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
{image}
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{message}
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
var React = require("react");
|
||||
var sanitizeHtml = require("sanitize-html");
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VectorCustomServerDialog',
|
||||
|
@ -26,24 +29,20 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
Custom Server Options
|
||||
{ _t('Custom Server Options') }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<span>
|
||||
You can use the custom server options to sign into other Matrix
|
||||
servers by specifying a different Home server URL.
|
||||
<br/>
|
||||
This allows you to use Riot with an existing Matrix account on
|
||||
a different home server.
|
||||
<br/>
|
||||
<br/>
|
||||
You can also set a custom identity server but you won't be able to
|
||||
invite users by email address, or be invited by email address yourself.
|
||||
</span>
|
||||
<span dangerouslySetInnerHTML={{__html: sanitizeHtml(_t(
|
||||
"You can use the custom server options to sign into other Matrix "+
|
||||
"servers by specifying a different Home server URL.<br/>This allows "+
|
||||
"you to use Riot with an existing Matrix account on a different home "+
|
||||
"server.<br/><br/>You can also set a custom identity server but you won't "+
|
||||
"be able to invite users by email address, or be invited by email address yourself.",
|
||||
))}} />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
Dismiss
|
||||
{ _t('Dismiss') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import SettingsStore from 'matrix-react-sdk/lib/settings/SettingsStore';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VectorLoginFooter',
|
||||
|
@ -25,12 +27,15 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
// FIXME: replace this with a proper Status skin
|
||||
if (SettingsStore.getValue("theme") === 'status') return <div/>;
|
||||
|
||||
return (
|
||||
<div className="mx_Login_links">
|
||||
<a href="https://medium.com/@RiotChat">blog</a> ·
|
||||
<a href="https://twitter.com/@RiotChat">twitter</a> ·
|
||||
<a href="https://github.com/vector-im/vector-web">github</a> ·
|
||||
<a href="https://matrix.org">powered by Matrix</a>
|
||||
<a href="https://github.com/vector-im/riot-web">github</a> ·
|
||||
<a href="https://matrix.org">{ _t('powered by Matrix') }</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,18 +16,27 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const i = [1, 2, 3, 4, 5][Math.floor(Math.random() * 5)];
|
||||
const DEFAULT_LOGO_URI = "img/logos/riot-im-logo-" + i + ".svg";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VectorLoginHeader',
|
||||
statics: {
|
||||
replaces: 'LoginHeader',
|
||||
},
|
||||
propTypes: {
|
||||
icon: PropTypes.string,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_Login_logo">
|
||||
<img src="img/logo.png" width="195" height="195" alt="Riot"/>
|
||||
<div className="mx_Login_header">
|
||||
<div className="mx_Login_logo">
|
||||
<img src={this.props.icon || DEFAULT_LOGO_URI} alt="Riot"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,19 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import React from 'react';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import DateUtils from 'matrix-react-sdk/lib/DateUtils';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var days = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
];
|
||||
function getdaysArray() {
|
||||
return [
|
||||
_t('Sunday'),
|
||||
_t('Monday'),
|
||||
_t('Tuesday'),
|
||||
_t('Wednesday'),
|
||||
_t('Thursday'),
|
||||
_t('Friday'),
|
||||
_t('Saturday'),
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'DateSeparator',
|
||||
|
@ -34,23 +36,24 @@ module.exports = React.createClass({
|
|||
var date = new Date(this.props.ts);
|
||||
var today = new Date();
|
||||
var yesterday = new Date();
|
||||
var days = getdaysArray();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
var label;
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
label = "Today";
|
||||
label = _t('Today');
|
||||
}
|
||||
else if (date.toDateString() === yesterday.toDateString()) {
|
||||
label = "Yesterday";
|
||||
label = _t('Yesterday');
|
||||
}
|
||||
else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
label = days[date.getDay()];
|
||||
}
|
||||
else {
|
||||
label = date.toDateString();
|
||||
label = DateUtils.formatFullDate(date, this.props.showTwelveHour);
|
||||
}
|
||||
|
||||
return (
|
||||
<h2>{ label }</h2>
|
||||
<h2 className="mx_DateSeparator">{ label }</h2>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,17 +16,23 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var DateUtils = require('matrix-react-sdk/lib/DateUtils');
|
||||
import React from 'react';
|
||||
import DateUtils from 'matrix-react-sdk/lib/DateUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageTimestamp',
|
||||
|
||||
propTypes: {
|
||||
showTwelveHour: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var date = new Date(this.props.ts);
|
||||
const date = new Date(this.props.ts);
|
||||
return (
|
||||
<span className="mx_MessageTimestamp">
|
||||
{ DateUtils.formatTime(date)+' ' }
|
||||
<span className="mx_MessageTimestamp" title={ DateUtils.formatFullDate(date, this.props.showTwelveHour) }>
|
||||
{ DateUtils.formatTime(date, this.props.showTwelveHour) + ' ' }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,14 +16,16 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var DragSource = require('react-dnd').DragSource;
|
||||
var DropTarget = require('react-dnd').DropTarget;
|
||||
import React from 'react';
|
||||
import {DragSource} from 'react-dnd';
|
||||
import {DropTarget} from 'react-dnd';
|
||||
|
||||
var dis = require("matrix-react-sdk/lib/dispatcher");
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var RoomTile = require('matrix-react-sdk/lib/components/views/rooms/RoomTile');
|
||||
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import RoomTile from 'matrix-react-sdk/lib/components/views/rooms/RoomTile';
|
||||
import * as Rooms from 'matrix-react-sdk/lib/Rooms';
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
|
||||
/**
|
||||
* Defines a new Component, DNDRoomTile that wraps RoomTile, making it draggable.
|
||||
|
@ -72,21 +74,49 @@ var roomTileSource = {
|
|||
item.targetList.forceUpdate(); // as we're not using state
|
||||
}
|
||||
|
||||
const prevTag = item.originalList.props.tagName;
|
||||
const newTag = item.targetList.props.tagName;
|
||||
|
||||
if (monitor.didDrop() && item.targetList.props.editable) {
|
||||
// Evil hack to get DMs behaving
|
||||
if ((prevTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(prevTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
) {
|
||||
Rooms.guessAndSetDMRoom(
|
||||
item.room, newTag === 'im.vector.fake.direct',
|
||||
).done(() => {
|
||||
item.originalList.removeRoomTile(item.room);
|
||||
}, (err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
|
||||
// if we moved lists, remove the old tag
|
||||
if (item.targetList !== item.originalList && item.originalList.props.tagName) {
|
||||
if (prevTag && prevTag !== 'im.vector.fake.direct' &&
|
||||
item.targetList !== item.originalList
|
||||
) {
|
||||
// commented out attempts to set a spinner on our target component as component is actually
|
||||
// the original source component being dragged, not our target. To fix we just need to
|
||||
// move all of this to endDrop in the target instead. FIXME later.
|
||||
|
||||
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
|
||||
MatrixClientPeg.get().deleteRoomTag(item.room.roomId, item.originalList.props.tagName).finally(function() {
|
||||
MatrixClientPeg.get().deleteRoomTag(item.room.roomId, prevTag).finally(function() {
|
||||
//component.state.set({ spinner: component.state.spinner-- });
|
||||
}).fail(function(err) {
|
||||
}).catch(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to remove tag " + item.originalList.props.tagName + " from room",
|
||||
description: err.toString()
|
||||
console.error("Failed to remove tag " + prevTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -97,15 +127,15 @@ var roomTileSource = {
|
|||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (item.targetList.props.tagName && (item.targetList !== item.originalList || newOrder)) {
|
||||
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
|
||||
MatrixClientPeg.get().setRoomTag(item.room.roomId, item.targetList.props.tagName, newOrder).finally(function() {
|
||||
//component.state.set({ spinner: component.state.spinner-- });
|
||||
}).fail(function(err) {
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
(item.targetList !== item.originalList || newOrder)
|
||||
) {
|
||||
MatrixClientPeg.get().setRoomTag(item.room.roomId, newTag, newOrder).catch(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to add tag " + item.targetList.props.tagName + " to room",
|
||||
description: err.toString()
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -208,4 +238,3 @@ DragSource('RoomTile', roomTileSource, function(connect, monitor) {
|
|||
isDragging: monitor.isDragging()
|
||||
};
|
||||
})(RoomTile));
|
||||
|
||||
|
|
|
@ -22,21 +22,12 @@ module.exports = React.createClass({
|
|||
displayName: 'RoomDropTarget',
|
||||
|
||||
render: function() {
|
||||
if (this.props.placeholder) {
|
||||
return (
|
||||
<div className="mx_RoomDropTarget mx_RoomDropTarget_placeholder">
|
||||
return (
|
||||
<div className="mx_RoomDropTarget">
|
||||
<div className="mx_RoomDropTarget_label">
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="mx_RoomDropTarget">
|
||||
<div className="mx_RoomDropTarget_avatar"></div>
|
||||
<div className="mx_RoomDropTarget_label">
|
||||
{ this.props.label }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,13 +19,16 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var dis = require('matrix-react-sdk/lib/dispatcher');
|
||||
import classNames from 'classnames';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTooltip',
|
||||
|
||||
propTypes: {
|
||||
// Alllow the tooltip to be styled by the parent element
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: React.PropTypes.string.isRequired,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: React.PropTypes.string,
|
||||
// The tooltip is derived from either the room name or a label
|
||||
room: React.PropTypes.object,
|
||||
label: React.PropTypes.string,
|
||||
|
@ -69,8 +72,12 @@ module.exports = React.createClass({
|
|||
style.left = 6 + parent.getBoundingClientRect().right + window.pageXOffset;
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames(
|
||||
"mx_RoomTooltip", this.props.tooltipClassName,
|
||||
);
|
||||
|
||||
var tooltip = (
|
||||
<div className="mx_RoomTooltip" style={style} >
|
||||
<div className={tooltipClasses} style={style} >
|
||||
<div className="mx_RoomTooltip_chevron"></div>
|
||||
{ label }
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,8 @@ var React = require('react');
|
|||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var classNames = require('classnames');
|
||||
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
|
||||
import { _t } from "matrix-react-sdk/lib/languageHandler";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SearchBar',
|
||||
|
@ -57,12 +59,12 @@ module.exports = React.createClass({
|
|||
var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' });
|
||||
|
||||
return (
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder="Search..." onKeyDown={this.onSearchChange}/>
|
||||
<div className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt="Search"/></div>
|
||||
<div className={ thisRoomClasses } onClick={this.onThisRoomClick}>This Room</div>
|
||||
<div className={ allRoomsClasses } onClick={this.onAllRoomsClick}>All Rooms</div>
|
||||
<img className="mx_SearchBar_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.props.onCancelClick} />
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange}/>
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")}/></AccessibleButton>
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,15 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
var React = require('react');
|
||||
var q = require("q");
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
|
||||
var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore');
|
||||
var Modal = require('matrix-react-sdk/lib/Modal');
|
||||
|
||||
var notifications = require('../../../notifications');
|
||||
import React from 'react';
|
||||
import Promise from 'bluebird';
|
||||
import sdk from 'matrix-react-sdk';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
|
||||
import UserSettingsStore from 'matrix-react-sdk/lib/UserSettingsStore';
|
||||
import SettingsStore, {SettingLevel} from "matrix-react-sdk/lib/settings/SettingsStore";
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import {
|
||||
NotificationUtils,
|
||||
VectorPushRulesDefinitions,
|
||||
PushRuleVectorState,
|
||||
ContentRules
|
||||
} from '../../../notifications';
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
// which should be factored out to other files.
|
||||
|
@ -30,17 +35,13 @@ var notifications = require('../../../notifications');
|
|||
// TODO: this component also does a lot of direct poking into this.state, which
|
||||
// is VERY NAUGHTY.
|
||||
|
||||
var NotificationUtils = notifications.NotificationUtils;
|
||||
var VectorPushRulesDefinitions = notifications.VectorPushRulesDefinitions;
|
||||
var PushRuleVectorState = notifications.PushRuleVectorState;
|
||||
var ContentRules = notifications.ContentRules;
|
||||
|
||||
/**
|
||||
* Rules that Vector used to set in order to override the actions of default rules.
|
||||
* These are used to port peoples existing overrides to match the current API.
|
||||
* These can be removed and forgotten once everyone has moved to the new client.
|
||||
*/
|
||||
var LEGACY_RULES = {
|
||||
const LEGACY_RULES = {
|
||||
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
|
||||
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
|
||||
"im.vector.rule.room_message": ".m.rule.message",
|
||||
|
@ -50,7 +51,7 @@ var LEGACY_RULES = {
|
|||
};
|
||||
|
||||
function portLegacyActions(actions) {
|
||||
var decoded = NotificationUtils.decodeActions(actions);
|
||||
const decoded = NotificationUtils.decodeActions(actions);
|
||||
if (decoded !== null) {
|
||||
return NotificationUtils.encodeActions(decoded);
|
||||
} else {
|
||||
|
@ -61,7 +62,7 @@ function portLegacyActions(actions) {
|
|||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Notififications',
|
||||
displayName: 'Notifications',
|
||||
|
||||
phases: {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
|
@ -101,7 +102,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onEnableNotificationsChange: function(event) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
});
|
||||
|
@ -112,41 +113,67 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onEnableDesktopNotificationsChange: function(event) {
|
||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
SettingsStore.setValue(
|
||||
"notificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
event.target.checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
|
||||
onEnableDesktopNotificationBodyChange: function(event) {
|
||||
SettingsStore.setValue(
|
||||
"notificationBodyEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
event.target.checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
|
||||
onEnableAudioNotificationsChange: function(event) {
|
||||
SettingsStore.setValue(
|
||||
"audioNotificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
event.target.checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
|
||||
onEnableEmailNotificationsChange: function(address, event) {
|
||||
var emailPusherPromise;
|
||||
let emailPusherPromise;
|
||||
if (event.target.checked) {
|
||||
var data = {}
|
||||
const data = {}
|
||||
data['brand'] = this.props.brand || 'Riot';
|
||||
emailPusherPromise = UserSettingsStore.addEmailPusher(address, data);
|
||||
} else {
|
||||
var emailPusher = UserSettingsStore.getEmailPusher(this.state.pushers, address);
|
||||
const emailPusher = UserSettingsStore.getEmailPusher(this.state.pushers, address);
|
||||
emailPusher.kind = null;
|
||||
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
|
||||
}
|
||||
emailPusherPromise.done(() => {
|
||||
this._refreshFromServer();
|
||||
}, (error) => {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error saving email notification preferences",
|
||||
description: "An error occurred whilst saving your email notification preferences.",
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, {
|
||||
title: _t('Error saving email notification preferences'),
|
||||
description: _t('An error occurred whilst saving your email notification preferences.'),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onNotifStateButtonClicked: function(event) {
|
||||
// FIXME: use .bind() rather than className metadata here surely
|
||||
var vectorRuleId = event.target.className.split("-")[0];
|
||||
var newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
const vectorRuleId = event.target.className.split("-")[0];
|
||||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
|
||||
if ("_keywords" === vectorRuleId) {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState)
|
||||
}
|
||||
else {
|
||||
var rule = this.getRule(vectorRuleId);
|
||||
const rule = this.getRule(vectorRuleId);
|
||||
if (rule) {
|
||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
||||
}
|
||||
|
@ -154,12 +181,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onKeywordsClicked: function(event) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
// Compute the keywords list to display
|
||||
var keywords = [];
|
||||
for (var i in this.state.vectorContentRules.rules) {
|
||||
var rule = this.state.vectorContentRules.rules[i];
|
||||
let keywords = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
keywords.push(rule.pattern);
|
||||
}
|
||||
if (keywords.length) {
|
||||
|
@ -173,16 +200,17 @@ module.exports = React.createClass({
|
|||
keywords = "";
|
||||
}
|
||||
|
||||
var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
Modal.createDialog(TextInputDialog, {
|
||||
title: "Keywords",
|
||||
description: "Enter keywords separated by a comma:",
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, {
|
||||
title: _t('Keywords'),
|
||||
description: _t('Enter keywords separated by a comma:'),
|
||||
button: _t('OK'),
|
||||
value: keywords,
|
||||
onFinished: function onFinished(should_leave, newValue) {
|
||||
|
||||
if (should_leave && newValue !== keywords) {
|
||||
var newKeywords = newValue.split(',');
|
||||
for (var i in newKeywords) {
|
||||
let newKeywords = newValue.split(',');
|
||||
for (let i in newKeywords) {
|
||||
newKeywords[i] = newKeywords[i].trim();
|
||||
}
|
||||
|
||||
|
@ -201,8 +229,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getRule: function(vectorRuleId) {
|
||||
for (var i in this.state.vectorPushRules) {
|
||||
var rule = this.state.vectorPushRules[i];
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.vectorRuleId === vectorRuleId) {
|
||||
return rule;
|
||||
}
|
||||
|
@ -216,13 +244,13 @@ module.exports = React.createClass({
|
|||
phase: this.phases.LOADING
|
||||
});
|
||||
|
||||
var self = this;
|
||||
var cli = MatrixClientPeg.get();
|
||||
var deferreds = [];
|
||||
var ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const deferreds = [];
|
||||
const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
|
||||
|
||||
if (rule.rule) {
|
||||
var actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
|
||||
const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
|
||||
|
||||
if (!actions) {
|
||||
// The new state corresponds to disabling the rule.
|
||||
|
@ -234,13 +262,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
q.all(deferreds).done(function() {
|
||||
Promise.all(deferreds).done(function() {
|
||||
self._refreshFromServer();
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Can't change settings",
|
||||
description: error.toString(),
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change settings: " + error);
|
||||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
||||
title: _t('Failed to change settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
});
|
||||
});
|
||||
|
@ -254,19 +283,19 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var cli = MatrixClientPeg.get();
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
});
|
||||
|
||||
// Update all rules in self.state.vectorContentRules
|
||||
var deferreds = [];
|
||||
for (var i in this.state.vectorContentRules.rules) {
|
||||
var rule = this.state.vectorContentRules.rules[i];
|
||||
const deferreds = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
|
||||
var enabled, actions;
|
||||
let enabled, actions;
|
||||
switch (newPushRuleVectorState) {
|
||||
case PushRuleVectorState.ON:
|
||||
if (rule.actions.length !== 1) {
|
||||
|
@ -303,13 +332,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
q.all(deferreds).done(function(resps) {
|
||||
Promise.all(deferreds).done(function(resps) {
|
||||
self._refreshFromServer();
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Can't update user notification settings",
|
||||
description: error.toString(),
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Can't update user notification settings: " + error);
|
||||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
||||
title: _t('Can\'t update user notification settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
});
|
||||
});
|
||||
|
@ -320,14 +350,14 @@ module.exports = React.createClass({
|
|||
phase: this.phases.LOADING
|
||||
});
|
||||
|
||||
var self = this;
|
||||
var cli = MatrixClientPeg.get();
|
||||
var removeDeferreds = [];
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const removeDeferreds = [];
|
||||
|
||||
// Remove per-word push rules of keywords that are no more in the list
|
||||
var vectorContentRulesPatterns = [];
|
||||
for (var i in self.state.vectorContentRules.rules) {
|
||||
var rule = self.state.vectorContentRules.rules[i];
|
||||
const vectorContentRulesPatterns = [];
|
||||
for (let i in self.state.vectorContentRules.rules) {
|
||||
const rule = self.state.vectorContentRules.rules[i];
|
||||
|
||||
vectorContentRulesPatterns.push(rule.pattern);
|
||||
|
||||
|
@ -338,28 +368,29 @@ module.exports = React.createClass({
|
|||
|
||||
// If the keyword is part of `externalContentRules`, remove the rule
|
||||
// before recreating it in the right Vector path
|
||||
for (var i in self.state.externalContentRules) {
|
||||
var rule = self.state.externalContentRules[i];
|
||||
for (let i in self.state.externalContentRules) {
|
||||
const rule = self.state.externalContentRules[i];
|
||||
|
||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
||||
}
|
||||
}
|
||||
|
||||
var onError = function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Can't update keywords",
|
||||
description: error.toString(),
|
||||
const onError = function(error) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to update keywords: " + error);
|
||||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
||||
title: _t('Failed to update keywords'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
});
|
||||
}
|
||||
|
||||
// Then, add the new ones
|
||||
q.all(removeDeferreds).done(function(resps) {
|
||||
var deferreds = [];
|
||||
Promise.all(removeDeferreds).done(function(resps) {
|
||||
const deferreds = [];
|
||||
|
||||
var pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
|
||||
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
|
||||
if (pushRuleVectorStateKind === PushRuleVectorState.OFF) {
|
||||
// When the current global keywords rule is OFF, we need to look at
|
||||
// the flavor of rules in 'vectorContentRules' to apply the same actions
|
||||
|
@ -374,8 +405,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
for (var i in newKeywords) {
|
||||
var keyword = newKeywords[i];
|
||||
for (let i in newKeywords) {
|
||||
const keyword = newKeywords[i];
|
||||
|
||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
||||
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
|
||||
|
@ -394,7 +425,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
q.all(deferreds).done(function(resps) {
|
||||
Promise.all(deferreds).done(function(resps) {
|
||||
self._refreshFromServer();
|
||||
}, onError);
|
||||
}, onError);
|
||||
|
@ -402,31 +433,33 @@ module.exports = React.createClass({
|
|||
|
||||
// Create a push rule but disabled
|
||||
_addDisabledPushRule: function(scope, kind, ruleId, body) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(function() {
|
||||
return cli.setPushRuleEnabled(scope, kind, ruleId, false);
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false)
|
||||
);
|
||||
},
|
||||
|
||||
// Check if any legacy im.vector rules need to be ported to the new API
|
||||
// for overriding the actions of default rules.
|
||||
_portRulesToNewAPI: function(rulesets) {
|
||||
var self = this;
|
||||
var needsUpdate = [];
|
||||
var cli = MatrixClientPeg.get();
|
||||
const self = this;
|
||||
const needsUpdate = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
for (var kind in rulesets.global) {
|
||||
var ruleset = rulesets.global[kind];
|
||||
for (var i = 0; i < ruleset.length; ++i) {
|
||||
var rule = ruleset[i];
|
||||
for (let kind in rulesets.global) {
|
||||
const ruleset = rulesets.global[kind];
|
||||
for (let i = 0; i < ruleset.length; ++i) {
|
||||
const rule = ruleset[i];
|
||||
if (rule.rule_id in LEGACY_RULES) {
|
||||
console.log("Porting legacy rule", rule);
|
||||
needsUpdate.push( function(kind, rule) {
|
||||
return cli.setPushRuleActions(
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
|
||||
).then( function() {
|
||||
return cli.deletePushRule('global', kind, rule.rule_id);
|
||||
})
|
||||
).then(() =>
|
||||
cli.deletePushRule('global', kind, rule.rule_id)
|
||||
).catch( (e) => {
|
||||
console.warn(`Error when porting legacy rule: ${e}`);
|
||||
});
|
||||
}(kind, rule));
|
||||
}
|
||||
}
|
||||
|
@ -435,9 +468,9 @@ module.exports = React.createClass({
|
|||
if (needsUpdate.length > 0) {
|
||||
// If some of the rules need to be ported then wait for the porting
|
||||
// to happen and then fetch the rules again.
|
||||
return q.allSettled(needsUpdate).then( function() {
|
||||
return cli.getPushRules();
|
||||
});
|
||||
return Promise.all(needsUpdate).then(() =>
|
||||
cli.getPushRules()
|
||||
);
|
||||
} else {
|
||||
// Otherwise return the rules that we already have.
|
||||
return rulesets;
|
||||
|
@ -445,21 +478,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_refreshFromServer: function() {
|
||||
var self = this;
|
||||
var pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
|
||||
//console.log("resolving pushRulesPromise");
|
||||
const self = this;
|
||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
|
||||
|
||||
/// XXX seriously? wtf is this?
|
||||
MatrixClientPeg.get().pushRules = rulesets;
|
||||
|
||||
// Get homeserver default rules and triage them by categories
|
||||
var rule_categories = {
|
||||
const rule_categories = {
|
||||
// The master rule (all notifications disabling)
|
||||
'.m.rule.master': 'master',
|
||||
|
||||
// The default push rules displayed by Vector UI
|
||||
// XXX: .m.rule.contains_user_name is not managed (not a fancy rule for Vector?)
|
||||
'.m.rule.contains_display_name': 'vector',
|
||||
'.m.rule.contains_user_name': 'vector',
|
||||
'.m.rule.room_one_to_one': 'vector',
|
||||
'.m.rule.message': 'vector',
|
||||
'.m.rule.invite_for_me': 'vector',
|
||||
|
@ -471,12 +503,12 @@ module.exports = React.createClass({
|
|||
};
|
||||
|
||||
// HS default rules
|
||||
var defaultRules = {master: [], vector: {}, others: []};
|
||||
const defaultRules = {master: [], vector: {}, others: []};
|
||||
|
||||
for (var kind in rulesets.global) {
|
||||
for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
var r = rulesets.global[kind][i];
|
||||
var cat = rule_categories[r.rule_id];
|
||||
for (let kind in rulesets.global) {
|
||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
const r = rulesets.global[kind][i];
|
||||
const cat = rule_categories[r.rule_id];
|
||||
r.kind = kind;
|
||||
|
||||
if (r.rule_id[0] === '.') {
|
||||
|
@ -499,7 +531,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// parse the keyword rules into our state
|
||||
var contentRules = ContentRules.parseContentRules(rulesets);
|
||||
const contentRules = ContentRules.parseContentRules(rulesets);
|
||||
self.state.vectorContentRules = {
|
||||
vectorState: contentRules.vectorState,
|
||||
rules: contentRules.rules,
|
||||
|
@ -510,8 +542,9 @@ module.exports = React.createClass({
|
|||
self.state.vectorPushRules = [];
|
||||
self.state.externalPushRules = [];
|
||||
|
||||
var vectorRuleIds = [
|
||||
const vectorRuleIds = [
|
||||
'.m.rule.contains_display_name',
|
||||
'.m.rule.contains_user_name',
|
||||
'_keywords',
|
||||
'.m.rule.room_one_to_one',
|
||||
'.m.rule.message',
|
||||
|
@ -520,8 +553,8 @@ module.exports = React.createClass({
|
|||
'.m.rule.call',
|
||||
'.m.rule.suppress_notices'
|
||||
];
|
||||
for (var i in vectorRuleIds) {
|
||||
var vectorRuleId = vectorRuleIds[i];
|
||||
for (let i in vectorRuleIds) {
|
||||
const vectorRuleId = vectorRuleIds[i];
|
||||
|
||||
if (vectorRuleId === '_keywords') {
|
||||
// keywords needs a special handling
|
||||
|
@ -529,21 +562,30 @@ module.exports = React.createClass({
|
|||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": "_keywords",
|
||||
"description" : (<span>Messages containing <span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>keywords</span></span>),
|
||||
"description" : (
|
||||
<span>
|
||||
{ _t('Messages containing <span>keywords</span>',
|
||||
{},
|
||||
{ 'span': (sub) =>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
"vectorState": self.state.vectorContentRules.vectorState
|
||||
});
|
||||
}
|
||||
else {
|
||||
var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
||||
var rule = defaultRules.vector[vectorRuleId];
|
||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
||||
const rule = defaultRules.vector[vectorRuleId];
|
||||
|
||||
var vectorState = ruleDefinition.ruleToVectorState(rule);
|
||||
const vectorState = ruleDefinition.ruleToVectorState(rule);
|
||||
|
||||
//console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState);
|
||||
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": vectorRuleId,
|
||||
"description" : ruleDefinition.description,
|
||||
"description" : _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"rule": rule,
|
||||
"vectorState": vectorState,
|
||||
});
|
||||
|
@ -557,14 +599,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// Build the rules not managed by Vector UI
|
||||
var otherRulesDescriptions = {
|
||||
'.m.rule.message': "Notify for all other messages/rooms",
|
||||
'.m.rule.fallback': "Notify me for anything else"
|
||||
const otherRulesDescriptions = {
|
||||
'.m.rule.message': _t('Notify for all other messages/rooms'),
|
||||
'.m.rule.fallback': _t('Notify me for anything else'),
|
||||
};
|
||||
|
||||
for (var i in defaultRules.others) {
|
||||
var rule = defaultRules.others[i];
|
||||
var ruleDescription = otherRulesDescriptions[rule.rule_id];
|
||||
for (let i in defaultRules.others) {
|
||||
const rule = defaultRules.others[i];
|
||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
||||
|
||||
// Show enabled default rules that was modified by the user
|
||||
if (ruleDescription && rule.enabled && !rule.default) {
|
||||
|
@ -574,16 +616,16 @@ module.exports = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
|
||||
//console.log("resolving pushersPromise");
|
||||
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
|
||||
self.setState({pushers: resp.pushers});
|
||||
});
|
||||
|
||||
q.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
self.setState({
|
||||
phase: self.phases.DISPLAY
|
||||
});
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
self.setState({
|
||||
phase: self.phases.ERROR
|
||||
});
|
||||
|
@ -600,7 +642,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updatePushRuleActions: function(rule, actions, enabled) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return cli.setPushRuleActions(
|
||||
'global', rule.kind, rule.rule_id, actions
|
||||
|
@ -618,7 +660,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<tr key={ className }>
|
||||
<th>
|
||||
{title}
|
||||
{ title }
|
||||
</th>
|
||||
|
||||
<th>
|
||||
|
@ -646,9 +688,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
renderNotifRulesTableRows: function() {
|
||||
var rows = [];
|
||||
for (var i in this.state.vectorPushRules) {
|
||||
var rule = this.state.vectorPushRules[i];
|
||||
const rows = [];
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
}
|
||||
|
@ -674,30 +716,32 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
var spinner;
|
||||
let spinner;
|
||||
if (this.state.phase === this.phases.LOADING) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
let masterPushRuleDiv;
|
||||
if (this.state.masterPushRule) {
|
||||
var masterPushRuleDiv = (
|
||||
masterPushRuleDiv = (
|
||||
<div className="mx_UserNotifSettings_tableRow">
|
||||
<div className="mx_UserNotifSettings_inputCell">
|
||||
<input id="enableNotifications"
|
||||
ref="enableNotifications"
|
||||
type="checkbox"
|
||||
checked={ !this.state.masterPushRule.enabled }
|
||||
onChange={ this.onEnableNotificationsChange } />
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_labelCell">
|
||||
<label htmlFor="enableNotifications">
|
||||
Enable notifications for this account
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_inputCell">
|
||||
<input id="enableNotifications"
|
||||
ref="enableNotifications"
|
||||
type="checkbox"
|
||||
checked={ !this.state.masterPushRule.enabled }
|
||||
onChange={ this.onEnableNotificationsChange }
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_labelCell">
|
||||
<label htmlFor="enableNotifications">
|
||||
{ _t('Enable notifications for this account') }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -709,57 +753,54 @@ module.exports = React.createClass({
|
|||
{masterPushRuleDiv}
|
||||
|
||||
<div className="mx_UserSettings_notifTable">
|
||||
All notifications are currently disabled for all targets.
|
||||
{ _t('All notifications are currently disabled for all targets.') }.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var emailNotificationsRow;
|
||||
if (this.props.threepids.filter(function(tp) {
|
||||
if (tp.medium == "email") {
|
||||
return true;
|
||||
}
|
||||
}).length == 0) {
|
||||
const emailThreepids = this.props.threepids.filter((tp) => tp.medium === "email");
|
||||
let emailNotificationsRow;
|
||||
if (emailThreepids.length === 0) {
|
||||
emailNotificationsRow = <div>
|
||||
Add an email address above to configure email notifications
|
||||
{ _t('Add an email address above to configure email notifications') }
|
||||
</div>;
|
||||
} else {
|
||||
// This only supports the first email address in your profile for now
|
||||
emailNotificationsRow = this.emailNotificationsRow(
|
||||
this.props.threepids[0].address,
|
||||
"Enable email notifications ("+this.props.threepids[0].address+")"
|
||||
emailThreepids[0].address,
|
||||
`${_t('Enable email notifications')} (${emailThreepids[0].address})`
|
||||
);
|
||||
}
|
||||
|
||||
// Build external push rules
|
||||
var externalRules = [];
|
||||
for (var i in this.state.externalPushRules) {
|
||||
var rule = this.state.externalPushRules[i];
|
||||
externalRules.push(<li>{ rule.description }</li>);
|
||||
const externalRules = [];
|
||||
for (let i in this.state.externalPushRules) {
|
||||
const rule = this.state.externalPushRules[i];
|
||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
||||
}
|
||||
|
||||
// Show keywords not displayed by the vector UI as a single external push rule
|
||||
var externalKeyWords = [];
|
||||
for (var i in this.state.externalContentRules) {
|
||||
var rule = this.state.externalContentRules[i];
|
||||
externalKeyWords.push(rule.pattern);
|
||||
let externalKeywords = [];
|
||||
for (let i in this.state.externalContentRules) {
|
||||
const rule = this.state.externalContentRules[i];
|
||||
externalKeywords.push(rule.pattern);
|
||||
}
|
||||
if (externalKeyWords.length) {
|
||||
externalKeyWords = externalKeyWords.join(", ");
|
||||
externalRules.push(<li>Notifications on the following keywords follow rules which can’t be displayed here: { externalKeyWords }</li>);
|
||||
if (externalKeywords.length) {
|
||||
externalKeywords = externalKeywords.join(", ");
|
||||
externalRules.push(<li>{ _t('Notifications on the following keywords follow rules which can’t be displayed here:') } { externalKeywords }</li>);
|
||||
}
|
||||
|
||||
var devicesSection;
|
||||
let devicesSection;
|
||||
if (this.state.pushers === undefined) {
|
||||
devicesSection = <div className="error">Unable to fetch notification target list</div>
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>
|
||||
} else if (this.state.pushers.length == 0) {
|
||||
devicesSection = null;
|
||||
} else {
|
||||
// TODO: It would be great to be able to delete pushers from here too,
|
||||
// and this wouldn't be hard to add.
|
||||
var rows = [];
|
||||
for (var i = 0; i < this.state.pushers.length; ++i) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < this.state.pushers.length; ++i) {
|
||||
rows.push(<tr key={ i }>
|
||||
<td>{this.state.pushers[i].app_display_name}</td>
|
||||
<td>{this.state.pushers[i].device_display_name}</td>
|
||||
|
@ -773,18 +814,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (devicesSection) {
|
||||
devicesSection = (<div>
|
||||
<h3>Notification targets</h3>
|
||||
<h3>{ _t('Notification targets') }</h3>
|
||||
{ devicesSection }
|
||||
</div>);
|
||||
}
|
||||
|
||||
var advancedSettings;
|
||||
let advancedSettings;
|
||||
if (externalRules.length) {
|
||||
advancedSettings = (
|
||||
<div>
|
||||
<h3>Advanced notifications settings</h3>
|
||||
There are advanced notifications which are not shown here.<br/>
|
||||
You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply.
|
||||
<h3>{ _t('Advanced notification settings') }</h3>
|
||||
{ _t('There are advanced notifications which are not shown here') }.<br/>
|
||||
{ _t('You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply') }.
|
||||
<ul>
|
||||
{ externalRules }
|
||||
</ul>
|
||||
|
@ -806,12 +847,27 @@ module.exports = React.createClass({
|
|||
<input id="enableDesktopNotifications"
|
||||
ref="enableDesktopNotifications"
|
||||
type="checkbox"
|
||||
checked={ UserSettingsStore.getEnableNotifications() }
|
||||
checked={ SettingsStore.getValue("notificationsEnabled") }
|
||||
onChange={ this.onEnableDesktopNotificationsChange } />
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_labelCell">
|
||||
<label htmlFor="enableDesktopNotifications">
|
||||
Enable desktop notifications
|
||||
{ _t('Enable desktop notifications') }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx_UserNotifSettings_tableRow">
|
||||
<div className="mx_UserNotifSettings_inputCell">
|
||||
<input id="enableDesktopNotificationBody"
|
||||
ref="enableDesktopNotificationBody"
|
||||
type="checkbox"
|
||||
checked={ SettingsStore.getValue("notificationBodyEnabled") }
|
||||
onChange={ this.onEnableDesktopNotificationBodyChange } />
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_labelCell">
|
||||
<label htmlFor="enableDesktopNotificationBody">
|
||||
{ _t('Show message in desktop notification') }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -821,15 +877,12 @@ module.exports = React.createClass({
|
|||
<input id="enableDesktopAudioNotifications"
|
||||
ref="enableDesktopAudioNotifications"
|
||||
type="checkbox"
|
||||
checked={ UserSettingsStore.getEnableAudioNotifications() }
|
||||
onChange={ (e) => {
|
||||
UserSettingsStore.setEnableAudioNotifications(e.target.checked);
|
||||
this.forceUpdate();
|
||||
}} />
|
||||
checked={ SettingsStore.getValue("audioNotificationsEnabled") }
|
||||
onChange={ this.onEnableAudioNotificationsChange } />
|
||||
</div>
|
||||
<div className="mx_UserNotifSettings_labelCell">
|
||||
<label htmlFor="enableDesktopAudioNotifications">
|
||||
Enable audible notifications in web client
|
||||
{ _t('Enable audible notifications in web client') }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -841,9 +894,9 @@ module.exports = React.createClass({
|
|||
<thead>
|
||||
<tr>
|
||||
<th width="55%"></th>
|
||||
<th width="15%">Off</th>
|
||||
<th width="15%">On</th>
|
||||
<th width="15%">Noisy</th>
|
||||
<th width="15%">{ _t('Off') }</th>
|
||||
<th width="15%">{ _t('On') }</th>
|
||||
<th width="15%">{ _t('Noisy') }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue