merge in develop
This commit is contained in:
commit
3b868064c7
1006 changed files with 78601 additions and 15855 deletions
197
src/components/structures/BottomLeftMenu.js
Normal file
197
src/components/structures/BottomLeftMenu.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Velocity from 'velocity-vector';
|
||||
import 'velocity-vector/velocity.ui';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
const CALLOUT_ANIM_DURATION = 1000;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BottomLeftMenu',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
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' });
|
||||
},
|
||||
|
||||
onDirectoryMouseEnter: function() {
|
||||
this.setState({ directoryHover: true });
|
||||
},
|
||||
|
||||
onDirectoryMouseLeave: function() {
|
||||
this.setState({ directoryHover: false });
|
||||
},
|
||||
|
||||
onRoomsClick: function() {
|
||||
dis.dispatch({ action: 'view_create_room' });
|
||||
},
|
||||
|
||||
onRoomsMouseEnter: function() {
|
||||
this.setState({ roomsHover: true });
|
||||
},
|
||||
|
||||
onRoomsMouseLeave: function() {
|
||||
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' });
|
||||
},
|
||||
|
||||
onPeopleMouseEnter: function() {
|
||||
this.setState({ peopleHover: true });
|
||||
},
|
||||
|
||||
onPeopleMouseLeave: function() {
|
||||
this.setState({ peopleHover: false });
|
||||
},
|
||||
|
||||
// Settings events
|
||||
onSettingsClick: function() {
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
},
|
||||
|
||||
onSettingsMouseEnter: function() {
|
||||
this.setState({ settingsHover: true });
|
||||
},
|
||||
|
||||
onSettingsMouseLeave: function() {
|
||||
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) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
|
||||
}
|
||||
},
|
||||
|
||||
_collectPeopleButton: function(e) {
|
||||
this._peopleButton = e;
|
||||
},
|
||||
|
||||
_collectDirectoryButton: function(e) {
|
||||
this._directoryButton = e;
|
||||
},
|
||||
|
||||
_collectCreateRoomButton: function(e) {
|
||||
this._createRoomButton = e;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
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 SettingsButton = sdk.getComponent('elements.SettingsButton');
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
|
||||
const groupsButton = SettingsStore.getValue("TagPanel.disableTagPanel") ?
|
||||
<GroupsButton tooltip={true} /> : null;
|
||||
|
||||
return (
|
||||
<div className="mx_BottomLeftMenu">
|
||||
<div className="mx_BottomLeftMenu_options">
|
||||
<HomeButton tooltip={true} />
|
||||
<div ref={this._collectPeopleButton}>
|
||||
<StartChatButton tooltip={true} />
|
||||
</div>
|
||||
<div ref={this._collectDirectoryButton}>
|
||||
<RoomDirectoryButton tooltip={true} />
|
||||
</div>
|
||||
<div ref={this._collectCreateRoomButton}>
|
||||
<CreateRoomButton tooltip={true} />
|
||||
</div>
|
||||
{ groupsButton }
|
||||
<span className="mx_BottomLeftMenu_settings">
|
||||
<SettingsButton tooltip={true} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
73
src/components/structures/CompatibilityPage.js
Normal file
73
src/components/structures/CompatibilityPage.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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 React = require('react');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CompatibilityPage',
|
||||
propTypes: {
|
||||
onAccept: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onAccept: function() {} // NOP
|
||||
};
|
||||
},
|
||||
|
||||
onAccept: function() {
|
||||
this.props.onAccept();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="mx_CompatibilityPage">
|
||||
<div className="mx_CompatibilityPage_box">
|
||||
<p>{ _t("Sorry, your browser is <b>not</b> able to run Riot.", {}, { 'b': (sub) => <b>{sub}</b> }) } </p>
|
||||
<p>
|
||||
{ _t("Riot uses many advanced browser features, some of which are not available or experimental in your current browser.") }
|
||||
</p>
|
||||
<p>
|
||||
{ _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>
|
||||
{ _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}>
|
||||
{ _t("I understand the risks and wish to continue") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,90 +16,176 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
module.exports = {
|
||||
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContextualMenuContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContextualMenuContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export default class ContextualMenu extends React.Component {
|
||||
propTypes: {
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
menuColour: React.PropTypes.string,
|
||||
},
|
||||
top: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
var container = document.getElementById(this.ContextualMenuContainerId);
|
||||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = this.ContextualMenuContainerId;
|
||||
document.body.appendChild(container);
|
||||
// The component to render as the context menu
|
||||
elementClass: PropTypes.element.isRequired,
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
contextMenuRect: null,
|
||||
};
|
||||
|
||||
this.onContextMenu = this.onContextMenu.bind(this);
|
||||
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||
}
|
||||
|
||||
collectContextMenuRect(element) {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contextMenuRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
if (this.props.closeMenu) {
|
||||
this.props.closeMenu();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
||||
const props = this.props;
|
||||
|
||||
if (props.top) {
|
||||
position.top = props.top;
|
||||
} else {
|
||||
position.bottom = props.bottom;
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
if (props.left) {
|
||||
position.left = props.left;
|
||||
chevronFace = 'left';
|
||||
} else {
|
||||
position.right = props.right;
|
||||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
createMenu: function(Element, props) {
|
||||
var self = this;
|
||||
const contextMenuRect = this.state.contextMenuRect || null;
|
||||
const padding = 10;
|
||||
|
||||
var closeMenu = function() {
|
||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
const target = position.top;
|
||||
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, arguments);
|
||||
// By default, no adjustment is made
|
||||
let adjusted = target;
|
||||
|
||||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
}
|
||||
};
|
||||
|
||||
var position = {
|
||||
top: props.top,
|
||||
};
|
||||
|
||||
var chevronOffset = {};
|
||||
if (props.chevronOffset) {
|
||||
chevronOffset.top = props.chevronOffset;
|
||||
position.top = adjusted;
|
||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
var chevronCSS = "";
|
||||
let chevronCSS = "";
|
||||
if (props.menuColour) {
|
||||
chevronCSS = `
|
||||
.mx_ContextualMenu_chevron_left:after {
|
||||
border-right-color: ${props.menuColour};
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_top:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_bottom:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
var chevron = null;
|
||||
if (props.left) {
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
|
||||
position.left = props.left;
|
||||
} else {
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
|
||||
position.right = props.right;
|
||||
}
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
var className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
var menuClasses = classNames({
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': props.left,
|
||||
'mx_ContextualMenu_right': !props.left,
|
||||
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
var menuStyle = {};
|
||||
const menuStyle = {};
|
||||
if (props.menuWidth) {
|
||||
menuStyle.width = props.menuWidth;
|
||||
}
|
||||
|
@ -111,21 +198,54 @@ module.exports = {
|
|||
menuStyle["backgroundColor"] = props.menuColour;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingLeft))) {
|
||||
menuStyle["paddingLeft"] = props.menuPaddingLeft;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingBottom))) {
|
||||
menuStyle["paddingBottom"] = props.menuPaddingBottom;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingRight))) {
|
||||
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||
}
|
||||
|
||||
const ElementClass = props.elementClass;
|
||||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
var menu = (
|
||||
<div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle}>
|
||||
{chevron}
|
||||
<Element {...props} onFinished={closeMenu}/>
|
||||
</div>
|
||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||
<style>{chevronCSS}</style>
|
||||
return <div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
);
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background"
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(menu, this.getOrCreateContainer());
|
||||
export function createMenu(ElementClass, props, hasBackground=true) {
|
||||
const closeMenu = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
return {close: closeMenu};
|
||||
},
|
||||
};
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, args);
|
||||
}
|
||||
};
|
||||
|
||||
// We only reference closeMenu once per call to createMenu
|
||||
const menu = <ContextualMenu
|
||||
hasBackground={hasBackground}
|
||||
{...props}
|
||||
elementClass={ElementClass}
|
||||
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
/>;
|
||||
|
||||
ReactDOM.render(menu, getOrCreateContainer());
|
||||
|
||||
return {close: closeMenu};
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
@ -30,8 +31,8 @@ module.exports = React.createClass({
|
|||
displayName: 'CreateRoom',
|
||||
|
||||
propTypes: {
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
onRoomCreated: PropTypes.func,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
phases: {
|
||||
|
@ -61,7 +62,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onCreateRoom: function() {
|
||||
var options = {};
|
||||
const options = {};
|
||||
|
||||
if (this.state.room_name) {
|
||||
options.name = this.state.room_name;
|
||||
|
@ -79,14 +80,14 @@ module.exports = React.createClass({
|
|||
{
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
"join_rule": this.state.is_private ? "invite" : "public"
|
||||
}
|
||||
"join_rule": this.state.is_private ? "invite" : "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
content: {
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited"
|
||||
}
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -94,19 +95,19 @@ module.exports = React.createClass({
|
|||
|
||||
options.invite = this.state.invited_users;
|
||||
|
||||
var alias = this.getAliasLocalpart();
|
||||
const alias = this.getAliasLocalpart();
|
||||
if (alias) {
|
||||
options.room_alias_name = alias;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
// TODO: Error.
|
||||
console.error("Cannot create room: No matrix client.");
|
||||
return;
|
||||
}
|
||||
|
||||
var deferred = cli.createRoom(options);
|
||||
const deferred = cli.createRoom(options);
|
||||
|
||||
if (this.state.encrypt) {
|
||||
// TODO
|
||||
|
@ -116,7 +117,7 @@ module.exports = React.createClass({
|
|||
phase: this.phases.CREATING,
|
||||
});
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
deferred.then(function(resp) {
|
||||
self.setState({
|
||||
|
@ -209,7 +210,7 @@ module.exports = React.createClass({
|
|||
|
||||
onAliasChanged: function(alias) {
|
||||
this.setState({
|
||||
alias: alias
|
||||
alias: alias,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -220,64 +221,64 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var curr_phase = this.state.phase;
|
||||
const curr_phase = this.state.phase;
|
||||
if (curr_phase == this.phases.CREATING) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader/>
|
||||
<Loader />
|
||||
);
|
||||
} else {
|
||||
var error_box = "";
|
||||
let error_box = "";
|
||||
if (curr_phase == this.phases.ERROR) {
|
||||
error_box = (
|
||||
<div className="mx_Error">
|
||||
{_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
|
||||
{ _t('An error occurred: %(error_string)s', {error_string: this.state.error_string}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||
var Presets = sdk.getComponent("create_room.Presets");
|
||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
var SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
||||
const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||
const RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||
const Presets = sdk.getComponent("create_room.Presets");
|
||||
const UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
||||
|
||||
var domain = MatrixClientPeg.get().getDomain();
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
return (
|
||||
<div className="mx_CreateRoom">
|
||||
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={ this.props.collapsedRhs }/>
|
||||
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={this.props.collapsedRhs} />
|
||||
<div className="mx_CreateRoom_body">
|
||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
|
||||
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
|
||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
|
||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')} /> <br />
|
||||
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')} /> <br />
|
||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={domain} onChange={this.onAliasChanged} /> <br />
|
||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged} /> <br />
|
||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset} /> <br />
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
||||
{_t('Make this room private')}
|
||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged} />
|
||||
{ _t('Make this room private') }
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
||||
{_t('Share message history with new users')}
|
||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged} />
|
||||
{ _t('Share message history with new users') }
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_CreateRoom_encrypt">
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
||||
{_t('Encrypt room')}
|
||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged} />
|
||||
{ _t('Encrypt room') }
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
||||
</div>
|
||||
{error_box}
|
||||
{ error_box }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,20 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
var FilePanel = React.createClass({
|
||||
const FilePanel = React.createClass({
|
||||
displayName: 'FilePanel',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -55,33 +56,36 @@ var FilePanel = React.createClass({
|
|||
},
|
||||
|
||||
updateTimelineSet: function(roomId) {
|
||||
var client = MatrixClientPeg.get();
|
||||
var room = client.getRoom(roomId);
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
this.noRoom = !room;
|
||||
|
||||
if (room) {
|
||||
var filter = new Matrix.Filter(client.credentials.userId);
|
||||
const filter = new Matrix.Filter(client.credentials.userId);
|
||||
filter.setDefinition(
|
||||
{
|
||||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true
|
||||
"contains_url": true,
|
||||
"types": [
|
||||
"m.room.message",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
||||
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
||||
(filterId)=>{
|
||||
filter.filterId = filterId;
|
||||
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||
this.setState({ timelineSet: timelineSet });
|
||||
},
|
||||
(error)=>{
|
||||
console.error("Failed to get or create file panel filter", error);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||
|
@ -92,18 +96,21 @@ var FilePanel = React.createClass({
|
|||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">
|
||||
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
} else if (this.noRoom) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
|
||||
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
if (this.state.timelineSet) {
|
||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
|
@ -114,17 +121,15 @@ var FilePanel = React.createClass({
|
|||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = { false }
|
||||
showUrlPreview = {false}
|
||||
tileShape="file_grid"
|
||||
opacity={ this.props.opacity }
|
||||
empty={_t('There are no visible files in this room')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_FilePanel">
|
||||
<Loader/>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
109
src/components/structures/HomePage.js
Normal file
109
src/components/structures/HomePage.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sdk from '../../index';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
static displayName = 'HomePage';
|
||||
|
||||
static propTypes = {
|
||||
// URL base of the team server. Optional.
|
||||
teamServerUrl: PropTypes.string,
|
||||
// Team token. Optional. If set, used to get the static homepage of the team
|
||||
// associated. If unset, homePageUrl will be used.
|
||||
teamToken: PropTypes.string,
|
||||
// URL to use as the iFrame src. Defaults to /home.html.
|
||||
homePageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
iframeSrc: '',
|
||||
page: '',
|
||||
};
|
||||
|
||||
translate(s) {
|
||||
// default implementation - skins may wish to extend this
|
||||
return sanitizeHtml(_t(s));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
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() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.iframeSrc) {
|
||||
return (
|
||||
<div className="mx_HomePage">
|
||||
<iframe src={ this.state.iframeSrc } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return (
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_HomePage">
|
||||
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomePage;
|
|
@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
|
|||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
|
@ -26,18 +27,18 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the auth process has finished,
|
||||
// successfully or unsuccessfully.
|
||||
|
@ -51,22 +52,22 @@ export default React.createClass({
|
|||
// the auth session.
|
||||
// * clientSecret {string} The client secret used in auth
|
||||
// sessions with the ID server.
|
||||
onAuthFinished: React.PropTypes.func.isRequired,
|
||||
onAuthFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Inputs provided by the user to the auth process
|
||||
// and used by various stages. As passed to js-sdk
|
||||
// interactive-auth
|
||||
inputs: React.PropTypes.object,
|
||||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: React.PropTypes.func,
|
||||
sessionId: React.PropTypes.string,
|
||||
clientSecret: React.PropTypes.string,
|
||||
emailSid: React.PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
||||
// If true, poll to see if the auth flow has been completed
|
||||
// out-of-band
|
||||
poll: React.PropTypes.bool,
|
||||
poll: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -107,7 +108,7 @@ export default React.createClass({
|
|||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
errorText: msg,
|
||||
});
|
||||
}).done();
|
||||
|
||||
|
@ -207,7 +208,7 @@ export default React.createClass({
|
|||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -215,8 +216,8 @@ export default React.createClass({
|
|||
return (
|
||||
<div>
|
||||
<div>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
{ this._renderCurrentStage() }
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
230
src/components/structures/LeftPanel.js
Normal file
230
src/components/structures/LeftPanel.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
|
||||
var LeftPanel = React.createClass({
|
||||
displayName: 'LeftPanel',
|
||||
|
||||
// NB. If you add props, don't forget to update
|
||||
// shouldComponentUpdate!
|
||||
propTypes: {
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
searchFilter: '',
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.focusedElement = null;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
_onFocus: function(ev) {
|
||||
this.focusedElement = ev.target;
|
||||
},
|
||||
|
||||
_onBlur: function(ev) {
|
||||
this.focusedElement = null;
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
let handled = true;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.TAB:
|
||||
this._onMoveFocus(ev.shiftKey);
|
||||
break;
|
||||
case KeyCode.UP:
|
||||
this._onMoveFocus(true);
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
this._onMoveFocus(false);
|
||||
break;
|
||||
case KeyCode.ENTER:
|
||||
this._onMoveFocus(false);
|
||||
if (this.focusedElement) {
|
||||
this.focusedElement.click();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_onMoveFocus: function(up) {
|
||||
let element = this.focusedElement;
|
||||
|
||||
// unclear why this isn't needed
|
||||
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||
// this.focusDirection = up;
|
||||
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const 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() {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
},
|
||||
|
||||
onSearch: function(term) {
|
||||
this.setState({ searchFilter: term });
|
||||
},
|
||||
|
||||
collectRoomList: function(ref) {
|
||||
this._roomList = ref;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomList = sdk.getComponent('rooms.RoomList');
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
let topBox;
|
||||
if (this.context.matrixClient.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 } />;
|
||||
}
|
||||
|
||||
const classes = classNames(
|
||||
"mx_LeftPanel",
|
||||
{
|
||||
"collapsed": this.props.collapsed,
|
||||
},
|
||||
);
|
||||
|
||||
const tagPanelEnabled = !SettingsStore.getValue("TagPanel.disableTagPanel");
|
||||
const tagPanel = tagPanelEnabled ? <TagPanel /> : <div />;
|
||||
|
||||
const containerClasses = classNames(
|
||||
"mx_LeftPanel_container", "mx_fadable",
|
||||
{
|
||||
"mx_LeftPanel_container_collapsed": this.props.collapsed,
|
||||
"mx_LeftPanel_container_hasTagPanel": tagPanelEnabled,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanel }
|
||||
<aside className={classes} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
{ topBox }
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<RoomList
|
||||
ref={this.collectRoomList}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
<BottomLeftMenu collapsed={this.props.collapsed}/>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LeftPanel;
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,9 +18,10 @@ limitations under the License.
|
|||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import KeyCode from '../../KeyCode';
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
|
@ -27,6 +29,16 @@ import sdk from '../../index';
|
|||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
|
@ -37,27 +49,27 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
export default React.createClass({
|
||||
const LoggedInView = React.createClass({
|
||||
displayName: 'LoggedInView',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: React.PropTypes.string.isRequired,
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
onUserSettingsClose: React.PropTypes.func,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
onUserSettingsClose: PropTypes.func,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
teamToken: React.PropTypes.string,
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: React.PropTypes.object,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
|
@ -73,7 +85,9 @@ export default React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -91,12 +105,18 @@ export default React.createClass({
|
|||
);
|
||||
this._setStateFromSessionStore();
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
|
@ -131,8 +151,61 @@ export default React.createClass({
|
|||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({action: "ignore_state_changed"});
|
||||
}
|
||||
},
|
||||
|
||||
onSync: function(syncState, oldSyncState, data) {
|
||||
const oldErrCode = this.state.syncErrorData && this.state.syncErrorData.error && this.state.syncErrorData.error.errcode;
|
||||
const newErrCode = data && data.error && data.error.errcode;
|
||||
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
||||
|
||||
if (syncState === 'ERROR') {
|
||||
this.setState({
|
||||
syncErrorData: data,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
syncErrorData: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
_updateServerNoticeEvents: async function() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (ev) pinnedEvents.push(ev);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -149,19 +222,13 @@ export default React.createClass({
|
|||
*/
|
||||
|
||||
let handled = false;
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
let ctrlCmdOnly;
|
||||
if (isMac) {
|
||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||
} else {
|
||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
let action = ev.keyCode == KeyCode.UP ?
|
||||
const action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
handled = true;
|
||||
|
@ -203,12 +270,71 @@ export default React.createClass({
|
|||
_onScrollKeyPressed: function(ev) {
|
||||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
}
|
||||
else if (this.refs.roomDirectory) {
|
||||
} else if (this.refs.roomDirectory) {
|
||||
this.refs.roomDirectory.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
_onDragEnd: function(result) {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the TagPanel receives an
|
||||
// optimistic update from TagOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomTileEndDrag: function(result) {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
// When the panels are disabled, clicking on them results in a mouse event
|
||||
// which bubbles to certain elements in the tree. When this happens, close
|
||||
// any settings page that is currently open (user/room/group).
|
||||
if (this.props.leftDisabled && this.props.rightDisabled) {
|
||||
const targetClasses = new Set(ev.target.className.split(' '));
|
||||
if (
|
||||
targetClasses.has('mx_MatrixChat') ||
|
||||
targetClasses.has('mx_MatrixChat_middlePanel') ||
|
||||
targetClasses.has('mx_RoomView')
|
||||
) {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
|
@ -220,9 +346,11 @@ export default React.createClass({
|
|||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
@ -237,22 +365,23 @@ export default React.createClass({
|
|||
oobData={this.props.roomOobData}
|
||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
opacity={this.props.middleOpacity}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) {
|
||||
right_panel = <RightPanel roomId={this.props.currentRoomId} disabled={this.props.rightDisabled} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
page_element = <UserSettings
|
||||
onClose={this.props.onUserSettingsClose}
|
||||
onClose={this.props.onCloseAllSettings}
|
||||
brand={this.props.config.brand}
|
||||
enableLabs={this.props.config.enableLabs}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
|
@ -262,9 +391,9 @@ export default React.createClass({
|
|||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
onRoomCreated={this.props.onRoomCreated}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.RoomDirectory:
|
||||
|
@ -292,19 +421,43 @@ export default React.createClass({
|
|||
|
||||
case PageTypes.UserView:
|
||||
page_element = null; // deliberately null for now
|
||||
right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||
right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
case PageTypes.GroupView:
|
||||
page_element = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
isNew={this.props.currentGroupIsNew}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
if (this.props.hasNewVersion) {
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
} else if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
|
@ -316,7 +469,7 @@ export default React.createClass({
|
|||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
var bodyClasses = 'mx_MatrixChat';
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
|
@ -325,20 +478,23 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
{topBar}
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
opacity={this.props.leftOpacity}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{page_element}
|
||||
</main>
|
||||
{right_panel}
|
||||
</div>
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default LoggedInView;
|
||||
|
|
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 '../../languageHandler';
|
||||
var sdk = require('../../index')
|
||||
var dis = require('../../dispatcher');
|
||||
var rate_limited_func = require('../../ratelimitedfunc');
|
||||
var AccessibleButton = require('../../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>
|
||||
);
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,14 +17,17 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
|
@ -32,66 +36,63 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: React.PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
// true to show a spinner at the top of the timeline to indicate
|
||||
// back-pagination in progress
|
||||
backPaginating: React.PropTypes.bool,
|
||||
backPaginating: PropTypes.bool,
|
||||
|
||||
// true to show a spinner at the end of the timeline to indicate
|
||||
// forward-pagination in progress
|
||||
forwardPaginating: React.PropTypes.bool,
|
||||
forwardPaginating: PropTypes.bool,
|
||||
|
||||
// the list of MatrixEvents to display
|
||||
events: React.PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// event after which we should show a read marker
|
||||
readMarkerEventId: React.PropTypes.string,
|
||||
readMarkerEventId: PropTypes.string,
|
||||
|
||||
// whether the read marker should be visible
|
||||
readMarkerVisible: React.PropTypes.bool,
|
||||
readMarkerVisible: PropTypes.bool,
|
||||
|
||||
// the userid of our user. This is used to suppress the read marker
|
||||
// for pending messages.
|
||||
ourUserId: React.PropTypes.string,
|
||||
ourUserId: PropTypes.string,
|
||||
|
||||
// true to suppress the date at the start of the timeline
|
||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
suppressFirstDateSeparator: PropTypes.bool,
|
||||
|
||||
// whether to show read receipts
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: PropTypes.bool,
|
||||
|
||||
// true if updates to the event list should cause the scroll panel to
|
||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||
// for more details.
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
// className for the panel
|
||||
className: React.PropTypes.string.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
|
||||
// shape parameter to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: React.PropTypes.bool,
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// show timestamps always
|
||||
alwaysShowTimestamps: React.PropTypes.bool,
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -112,8 +113,6 @@ module.exports = React.createClass({
|
|||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
|
||||
|
@ -154,15 +153,15 @@ module.exports = React.createClass({
|
|||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
var readMarker = this.refs.readMarkerNode;
|
||||
var messageWrapper = this.refs.scrollPanel;
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
|
||||
if (!readMarker || !messageWrapper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
var readMarkerRect = readMarker.getBoundingClientRect();
|
||||
const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
const readMarkerRect = readMarker.getBoundingClientRect();
|
||||
|
||||
// the read-marker pretends to have zero height when it is actually
|
||||
// two pixels high; +2 here to account for that.
|
||||
|
@ -194,7 +193,7 @@ module.exports = React.createClass({
|
|||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* mult: -1 to page up, +1 to page down
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -204,6 +203,8 @@ module.exports = React.createClass({
|
|||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*
|
||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -241,6 +242,10 @@ module.exports = React.createClass({
|
|||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
_shouldShowEvent: function(mxEv) {
|
||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
|
@ -249,7 +254,7 @@ module.exports = React.createClass({
|
|||
// Always show highlighted event
|
||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv, this._syncedSettings);
|
||||
return !shouldHideEvent(mxEv);
|
||||
},
|
||||
|
||||
_getEventTiles: function() {
|
||||
|
@ -258,7 +263,8 @@ module.exports = React.createClass({
|
|||
|
||||
this.eventNodes = {};
|
||||
|
||||
var i;
|
||||
let visible = false;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
// actually going to show; this allows us to behave slightly
|
||||
|
@ -268,9 +274,9 @@ module.exports = React.createClass({
|
|||
// a local echo, to manage the read-marker.
|
||||
let lastShownEvent;
|
||||
|
||||
var lastShownNonLocalEchoIndex = -1;
|
||||
let lastShownNonLocalEchoIndex = -1;
|
||||
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||
var mxEv = this.props.events[i];
|
||||
const mxEv = this.props.events[i];
|
||||
if (!this._shouldShowEvent(mxEv)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -288,33 +294,33 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
|
||||
var ret = [];
|
||||
const ret = [];
|
||||
|
||||
var prevEvent = null; // the last event we showed
|
||||
let prevEvent = null; // the last event we showed
|
||||
|
||||
// assume there is no read marker until proven otherwise
|
||||
var readMarkerVisible = false;
|
||||
let readMarkerVisible = false;
|
||||
|
||||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
this.props.readMarkerVisible &&
|
||||
this.currentReadMarkerEventId != this.props.readMarkerEventId) {
|
||||
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||
this.currentGhostEventId = null;
|
||||
}
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
let mxEv = this.props.events[i];
|
||||
let eventId = mxEv.getId();
|
||||
let last = (mxEv === lastShownEvent);
|
||||
const mxEv = this.props.events[i];
|
||||
const eventId = mxEv.getId();
|
||||
const last = (mxEv === lastShownEvent);
|
||||
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && wantTile) {
|
||||
let readMarkerInMels = false;
|
||||
let ts1 = mxEv.getTs();
|
||||
const ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||
|
@ -326,7 +332,7 @@ module.exports = React.createClass({
|
|||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
|
@ -335,7 +341,7 @@ module.exports = React.createClass({
|
|||
readMarkerInMels = true;
|
||||
}
|
||||
|
||||
let summarisedEvents = [mxEv];
|
||||
const summarisedEvents = [mxEv];
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
const collapsedMxEv = this.props.events[i + 1];
|
||||
|
||||
|
@ -349,7 +355,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (!isMembershipChange(collapsedMxEv) ||
|
||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||
this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -361,35 +367,37 @@ module.exports = React.createClass({
|
|||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
||||
let highlightInMels = false;
|
||||
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
let eventTiles = summarisedEvents.map((e) => {
|
||||
if (e.getId() === this.props.highlightedEventId) {
|
||||
highlightInMels = true;
|
||||
}
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeperator is inserted.
|
||||
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
prevEvent = e;
|
||||
return ret;
|
||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
}).reduce((a, b) => a.concat(b));
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
ret.push(<MemberEventListSummary key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
>
|
||||
{ eventTiles }
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -401,10 +409,10 @@ module.exports = React.createClass({
|
|||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
var isVisibleReadMarker = false;
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId == this.props.readMarkerEventId) {
|
||||
var visible = this.props.readMarkerVisible;
|
||||
if (eventId === this.props.readMarkerEventId) {
|
||||
visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
|
@ -422,11 +430,11 @@ module.exports = React.createClass({
|
|||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId == this.currentGhostEventId) {
|
||||
if (eventId === this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
} else if (!isVisibleReadMarker &&
|
||||
eventId == this.currentReadMarkerEventId) {
|
||||
eventId === this.currentReadMarkerEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
@ -441,15 +449,24 @@ module.exports = React.createClass({
|
|||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
var ret = [];
|
||||
const ret = [];
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
let continuation = false;
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()) {
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
|
@ -469,8 +486,8 @@ module.exports = React.createClass({
|
|||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
var ts1 = mxEv.getTs();
|
||||
var eventDate = mxEv.getDate();
|
||||
let ts1 = mxEv.getTs();
|
||||
let eventDate = mxEv.getDate();
|
||||
if (mxEv.status) {
|
||||
eventDate = new Date();
|
||||
ts1 = eventDate.getTime();
|
||||
|
@ -478,19 +495,19 @@ module.exports = React.createClass({
|
|||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
var eventId = mxEv.getId();
|
||||
var highlight = (eventId == this.props.highlightedEventId);
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
var scrollToken = mxEv.status ? undefined : eventId;
|
||||
const scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
var readReceipts;
|
||||
let readReceipts;
|
||||
if (this.props.showReadReceipts) {
|
||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
}
|
||||
|
@ -508,8 +525,8 @@ module.exports = React.createClass({
|
|||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
last={last} isSelectedEvent={highlight}/>
|
||||
</li>
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
</li>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
|
@ -521,17 +538,7 @@ module.exports = React.createClass({
|
|||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare weekdays
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
|
@ -544,12 +551,15 @@ module.exports = React.createClass({
|
|||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let receipts = [];
|
||||
const receipts = [];
|
||||
room.getReceiptsForEvent(event).forEach((r) => {
|
||||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||
return; // ignore non-read receipts and receipts from self.
|
||||
}
|
||||
let member = room.getMember(r.userId);
|
||||
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
|
||||
return; // ignore ignored users
|
||||
}
|
||||
const member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
}
|
||||
|
@ -565,7 +575,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getReadMarkerTile: function(visible) {
|
||||
var hr;
|
||||
let hr;
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
|
@ -575,7 +585,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<li key="_readupto" ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{hr}
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
@ -594,7 +604,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getReadMarkerGhostTile: function() {
|
||||
var hr = <hr className="mx_RoomView_myReadMarker"
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
ref={this._startAnimation}
|
||||
/>;
|
||||
|
@ -605,7 +615,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{hr}
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
@ -617,7 +627,7 @@ module.exports = React.createClass({
|
|||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onWidgetLoad: function() {
|
||||
var scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.forceUpdate();
|
||||
}
|
||||
|
@ -628,9 +638,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
var topSpinner, bottomSpinner;
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let topSpinner;
|
||||
let bottomSpinner;
|
||||
if (this.props.backPaginating) {
|
||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
@ -638,25 +649,26 @@ module.exports = React.createClass({
|
|||
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
||||
var style = this.props.hidden ? { display: 'none' } : {};
|
||||
style.opacity = this.props.opacity;
|
||||
const style = this.props.hidden ? { display: 'none' } : {};
|
||||
|
||||
var className = this.props.className + " mx_fadable";
|
||||
if (this.props.alwaysShowTimestamps) {
|
||||
className += " mx_MessagePanel_alwaysShowTimestamps";
|
||||
}
|
||||
const className = classNames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className={ className }
|
||||
onScroll={ this.props.onScroll }
|
||||
onResize={ this.onResize }
|
||||
onFillRequest={ this.props.onFillRequest }
|
||||
onUnfillRequest={ this.props.onUnfillRequest }
|
||||
style={ style }
|
||||
stickyBottom={ this.props.stickyBottom }>
|
||||
{topSpinner}
|
||||
{this._getEventTiles()}
|
||||
{bottomSpinner}
|
||||
<ScrollPanel ref="scrollPanel" className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,39 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../index';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import dis from '../../dispatcher';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../Modal';
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
||||
},
|
||||
});
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'MyGroups',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -62,14 +41,18 @@ export default withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_onCreateGroupClick: function() {
|
||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||
Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
|
||||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
_fetch: function() {
|
||||
this.props.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.setState({groups: result.groups, error: null});
|
||||
}, (err) => {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
// Indicate that the guest isn't in any groups (which should be true)
|
||||
this.setState({groups: [], error: null});
|
||||
return;
|
||||
}
|
||||
this.setState({groups: null, error: err});
|
||||
});
|
||||
},
|
||||
|
@ -78,63 +61,90 @@ export default withMatrixClient(React.createClass({
|
|||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
||||
let content;
|
||||
let contentHeader;
|
||||
if (this.state.groups) {
|
||||
const groupNodes = [];
|
||||
this.state.groups.forEach((g) => {
|
||||
groupNodes.push(
|
||||
<div key={g}>
|
||||
<GroupTile groupId={g} />
|
||||
</div>,
|
||||
);
|
||||
groupNodes.push(<GroupTile key={g} groupId={g} />);
|
||||
});
|
||||
content = <div>
|
||||
<div>{_t('You are a member of these groups:')}</div>
|
||||
{groupNodes}
|
||||
</div>;
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbarWrapper>
|
||||
<div className="mx_MyGroups_microcopy">
|
||||
<p>
|
||||
{ _t(
|
||||
"Did you know: you can use communities to filter your Riot.im experience!",
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
||||
"filter panel at any time to see only the rooms and people associated " +
|
||||
"with that community.",
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
) }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
content = <div className="mx_MyGroups_error">
|
||||
{_t('Error whilst fetching joined groups')}
|
||||
{ _t('Error whilst fetching joined communities') }
|
||||
</div>;
|
||||
} else {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return <div className="mx_MyGroups">
|
||||
<SimpleRoomHeader title={ _t("Groups") } />
|
||||
<div className='mx_MyGroups_joinCreateBox'>
|
||||
<div className="mx_MyGroups_createBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Create a new group')}
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||
<SimpleRoomHeader title={_t("Communities")} icon="img/icons-groups.svg" />
|
||||
<div className='mx_MyGroups_header'>
|
||||
<div className="mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_t(
|
||||
'Create a group to represent your community! '+
|
||||
'Define a set of rooms and your own custom homepage '+
|
||||
'to mark out your space in the Matrix universe.',
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Join an existing group')}
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Create a new community') }
|
||||
</div>
|
||||
{ _t(
|
||||
'Create a community to group together users and rooms! ' +
|
||||
'Build a custom homepage to mark out your space in the Matrix universe.',
|
||||
) }
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||
</div>
|
||||
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_tJsx(
|
||||
'To join an existing group you\'ll have to '+
|
||||
'know its group identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{sub}</i>,
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Join an existing community') }
|
||||
</div>
|
||||
{ _t(
|
||||
'To join an existing community you\'ll have to '+
|
||||
'know its community identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
{},
|
||||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{content}
|
||||
{ contentHeader }
|
||||
{ content }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import { _t } from '../../languageHandler';
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var dis = require("../../dispatcher");
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
var NotificationPanel = React.createClass({
|
||||
const NotificationPanel = React.createClass({
|
||||
displayName: 'NotificationPanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
|
|||
|
||||
render: function() {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
return (
|
||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||
|
@ -44,18 +44,16 @@ var NotificationPanel = React.createClass({
|
|||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview = { false }
|
||||
opacity={ this.props.opacity }
|
||||
showUrlPreview = {false}
|
||||
tileShape="notif"
|
||||
empty={ _t('You have no visible notifications') }
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
return (
|
||||
<div className="mx_NotificationPanel">
|
||||
<Loader/>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
424
src/components/structures/RightPanel.js
Normal file
424
src/components/structures/RightPanel.js
Normal file
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import Analytics from '../../Analytics';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
|
||||
import { formatCount } from '../../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 /> }
|
||||
|
||||
</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: {
|
||||
// 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
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
Phase: {
|
||||
RoomMemberList: 'RoomMemberList',
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
GroupRoomInfo: 'GroupRoomInfo',
|
||||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context.matrixClient;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.context.matrixClient) {
|
||||
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.props.groupId ? this.Phase.GroupMemberList : this.Phase.RoomMemberList,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
if (!groupId) return;
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
_unregisterGroupStore() {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
onGroupStoreUpdated: function() {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||
});
|
||||
},
|
||||
|
||||
onCollapseClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
},
|
||||
|
||||
onInviteButtonClick: function() {
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
// call AddressPickerDialog
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
onInviteToGroupButtonClick: function() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberList,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onAddRoomToGroupButtonClick: function() {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
this._delayedUpdate();
|
||||
} 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();
|
||||
}
|
||||
},
|
||||
|
||||
_delayedUpdate: new RateLimitedFunc(function() {
|
||||
this.forceUpdate(); // eslint-disable-line babel/no-invalid-this
|
||||
}, 500),
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action === "view_user") {
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
if (payload.member) {
|
||||
this.setState({
|
||||
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 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_member_list") {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberList,
|
||||
});
|
||||
} 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() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let inviteGroup;
|
||||
|
||||
let membersBadge;
|
||||
let membersTitle = _t('Members');
|
||||
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 isUserInRoom;
|
||||
if (room) {
|
||||
const numMembers = room.getJoinedMemberCount();
|
||||
membersTitle = _t('%(count)s Members', { count: numMembers });
|
||||
membersBadge = <div title={membersTitle}>{ formatCount(numMembers) }</div>;
|
||||
isUserInRoom = room.hasMembershipState(this.context.matrixClient.credentials.userId, 'join');
|
||||
}
|
||||
|
||||
if (isUserInRoom) {
|
||||
inviteGroup =
|
||||
<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 room') }</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
||||
|
||||
const isPhaseGroup = [
|
||||
this.Phase.GroupMemberInfo,
|
||||
this.Phase.GroupMemberList,
|
||||
].includes(this.state.phase);
|
||||
|
||||
let headerButtons = [];
|
||||
if (this.props.roomId) {
|
||||
headerButtons = [
|
||||
<HeaderButton key="_membersButton" title={membersTitle} 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.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} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (!panel) {
|
||||
panel = <div className="mx_RightPanel_blank" />;
|
||||
}
|
||||
|
||||
if (this.props.groupId && this.state.isUserPrivilegedInGroup) {
|
||||
inviteGroup = isPhaseGroup ? (
|
||||
<AccessibleButton className="mx_RightPanel_invite" onClick={this.onInviteToGroupButtonClick}>
|
||||
<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.onAddRoomToGroupButtonClick}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
"collapsed": this.props.collapsed,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<aside className={classes}>
|
||||
<div className="mx_RightPanel_header">
|
||||
<div className="mx_RightPanel_headerButtonGroup">
|
||||
{ headerButtons }
|
||||
</div>
|
||||
</div>
|
||||
{ panel }
|
||||
<div className="mx_RightPanel_footer">
|
||||
{ inviteGroup }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
},
|
||||
});
|
587
src/components/structures/RoomDirectory.js
Normal file
587
src/components/structures/RoomDirectory.js
Normal file
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
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 React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var Modal = require('../../Modal');
|
||||
var sdk = require('../../index');
|
||||
var dis = require('../../dispatcher');
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyString = require('linkifyjs/string');
|
||||
var linkifyMatrix = require('../../linkify-matrix');
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomDirectory',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
config: {},
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
protocolsLoading: true,
|
||||
instanceId: null,
|
||||
includeAll: false,
|
||||
roomServer: null,
|
||||
filterString: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
this.scrollPanel = null;
|
||||
this.protocols = null;
|
||||
|
||||
this.setState({protocolsLoading: true});
|
||||
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
|
||||
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
|
||||
// ignore this as otherwise this error is literally the
|
||||
// thing you see when loading the client!
|
||||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
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: 'panel_disable',
|
||||
// sideDisabled: true,
|
||||
// middleDisabled: true,
|
||||
// });
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// dis.dispatch({
|
||||
// action: 'panel_disable',
|
||||
// sideDisabled: false,
|
||||
// middleDisabled: false,
|
||||
// });
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
refreshRoomList: function() {
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
});
|
||||
this.getMoreRooms().done();
|
||||
},
|
||||
|
||||
getMoreRooms: function() {
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
|
||||
const my_filter_string = this.state.filterString;
|
||||
const my_server = this.state.roomServer;
|
||||
// remember the next batch token when we sent the request
|
||||
// too. If it's changed, appending to the list will corrupt it.
|
||||
const my_next_batch = this.nextBatch;
|
||||
const opts = {limit: 20};
|
||||
if (my_server != MatrixClientPeg.getHomeServerName()) {
|
||||
opts.server = my_server;
|
||||
}
|
||||
if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
} else if (this.state.includeAll) {
|
||||
opts.include_all_networks = true;
|
||||
}
|
||||
if (this.nextBatch) opts.since = this.nextBatch;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
|
||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch)
|
||||
{
|
||||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
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);
|
||||
s.loading = false;
|
||||
return s;
|
||||
});
|
||||
return Boolean(data.next_batch);
|
||||
}, (err) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch)
|
||||
{
|
||||
// as above: we don't care about errors for old
|
||||
// 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.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'))
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* A limited interface for removing rooms from the directory.
|
||||
* Will set the room to not be publicly visible and delete the
|
||||
* default alias. In the long term, it would be better to allow
|
||||
* HS admins to do this through the RoomSettings interface, but
|
||||
* this needs SPEC-417.
|
||||
*/
|
||||
removeFromDirectory: function(room) {
|
||||
var alias = get_display_alias_for_room(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 = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
} else {
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
||||
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 = _t('remove %(name)s from the directory.', {name: name});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).done(() => {
|
||||
modal.close();
|
||||
this.refreshRoomList();
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
this.refreshRoomList();
|
||||
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'))
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onRoomClicked: function(room, ev) {
|
||||
if (ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
this.removeFromDirectory(room);
|
||||
} else {
|
||||
this.showRoom(room);
|
||||
}
|
||||
},
|
||||
|
||||
onOptionChange: function(server, instanceId, includeAll) {
|
||||
// clear next batch so we don't try to load more rooms
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
// Clear the public rooms out here otherwise we needlessly
|
||||
// spend time filtering lots of rooms when we're about to
|
||||
// to clear the list anyway.
|
||||
publicRooms: [],
|
||||
roomServer: server,
|
||||
instanceId: instanceId,
|
||||
includeAll: includeAll,
|
||||
}, this.refreshRoomList);
|
||||
// We also refresh the room list each time even though this
|
||||
// filtering is client-side. It hopefully won't be client side
|
||||
// for very long, and we may have fetched a thousand rooms to
|
||||
// find the five gitter ones, at which point we do not want
|
||||
// to render all those rooms when switching back to 'all networks'.
|
||||
// Easiest to just blow away the state & re-fetch.
|
||||
},
|
||||
|
||||
onFillRequest: function(backwards) {
|
||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||
|
||||
return this.getMoreRooms();
|
||||
},
|
||||
|
||||
onFilterChange: function(alias) {
|
||||
this.setState({
|
||||
filterString: alias || null,
|
||||
});
|
||||
|
||||
// don't send the request for a little bit,
|
||||
// no point hammering the server with a
|
||||
// request for every keystroke, let the
|
||||
// user finish typing.
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
this.filterTimeout = setTimeout(() => {
|
||||
this.filterTimeout = null;
|
||||
this.refreshRoomList();
|
||||
}, 700);
|
||||
},
|
||||
|
||||
onFilterClear: function() {
|
||||
// update immediately
|
||||
this.setState({
|
||||
filterString: null,
|
||||
}, this.refreshRoomList);
|
||||
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
},
|
||||
|
||||
onJoinClick: function(alias) {
|
||||
// If we don't have a particular instance id selected, just show that rooms alias
|
||||
if (!this.state.instanceId) {
|
||||
// If the user specified an alias without a domain, add on whichever server is selected
|
||||
// in the dropdown
|
||||
if (alias.indexOf(':') == -1) {
|
||||
alias = alias + ':' + this.state.roomServer;
|
||||
}
|
||||
this.showRoomAlias(alias);
|
||||
} else {
|
||||
// This is a 3rd party protocol. Let's see if we can join it
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
||||
if (!fields) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
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;
|
||||
}
|
||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
||||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
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.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
||||
title: _t('Fetching third party location failed'),
|
||||
description: _t('Unable to look up room ID from server'),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias) {
|
||||
this.showRoom(null, alias);
|
||||
},
|
||||
|
||||
showRoom: function(room, room_alias) {
|
||||
var payload = {action: 'view_room'};
|
||||
if (room) {
|
||||
// Don't let the user view a room they won't be able to either
|
||||
// peek or join: fail earlier so they don't have to click back
|
||||
// to the directory.
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (!room.world_readable && !room.guest_can_join) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!room_alias) {
|
||||
room_alias = get_display_alias_for_room(room);
|
||||
}
|
||||
|
||||
payload.oob_data = {
|
||||
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 || _t('Unnamed room'),
|
||||
};
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
// this list without aliases at present, so if roomAlias isn't set here we have no
|
||||
// choice but to supply the ID.
|
||||
if (room_alias) {
|
||||
payload.room_alias = room_alias;
|
||||
} else {
|
||||
payload.room_id = room.room_id;
|
||||
}
|
||||
dis.dispatch(payload);
|
||||
},
|
||||
|
||||
getRows: function() {
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
if (!this.state.publicRooms) return [];
|
||||
|
||||
var rooms = this.state.publicRooms;
|
||||
var rows = [];
|
||||
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]) || _t('Unnamed room');
|
||||
guestRead = null;
|
||||
guestJoin = null;
|
||||
|
||||
if (rooms[i].world_readable) {
|
||||
guestRead = (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
);
|
||||
}
|
||||
if (rooms[i].guest_can_join) {
|
||||
guestJoin = (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
);
|
||||
}
|
||||
|
||||
perms = null;
|
||||
if (guestRead || guestJoin) {
|
||||
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
|
||||
}
|
||||
|
||||
var topic = rooms[i].topic || '';
|
||||
topic = linkifyString(sanitizeHtml(topic));
|
||||
|
||||
rows.push(
|
||||
<tr key={ rooms[i].room_id }
|
||||
onClick={self.onRoomClicked.bind(self, rooms[i])}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
rooms[i].avatar_url, 24, 24, "crop") } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
{ perms }
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ function(e) { e.stopPropagation() } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }}/>
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ rooms[i].num_joined_members }
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
|
||||
collectScrollPanel: function(element) {
|
||||
this.scrollPanel = element;
|
||||
},
|
||||
|
||||
_stringLooksLikeId: function(s, field_type) {
|
||||
let pat = /^#[^\s]+:[^\s]/;
|
||||
if (field_type && field_type.regexp) {
|
||||
pat = new RegExp(field_type.regexp);
|
||||
}
|
||||
|
||||
return pat.test(s);
|
||||
},
|
||||
|
||||
_getFieldsForThirdPartyLocation: function(userInput, protocol, instance) {
|
||||
// make an object with the fields specified by that protocol. We
|
||||
// require that the values of all but the last field come from the
|
||||
// instance. The last is the user input.
|
||||
const requiredFields = protocol.location_fields;
|
||||
if (!requiredFields) return null;
|
||||
const fields = {};
|
||||
for (let i = 0; i < requiredFields.length - 1; ++i) {
|
||||
const thisField = requiredFields[i];
|
||||
if (instance.fields[thisField] === undefined) return null;
|
||||
fields[thisField] = instance.fields[thisField];
|
||||
}
|
||||
fields[requiredFields[requiredFields.length - 1]] = userInput;
|
||||
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");
|
||||
|
||||
if (this.state.protocolsLoading) {
|
||||
return (
|
||||
<div className="mx_RoomDirectory">
|
||||
<SimpleRoomHeader title={ _t('Directory') } />
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.loading) {
|
||||
content = <div className="mx_RoomDirectory">
|
||||
<Loader />
|
||||
</div>;
|
||||
} else {
|
||||
const rows = this.getRows();
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
let scrollpanel_content;
|
||||
if (rows.length == 0) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ this.getRows() }
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
className="mx_RoomDirectory_tableWrapper"
|
||||
onFillRequest={ this.onFillRequest }
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
onResize={function(){}}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
</ScrollPanel>;
|
||||
}
|
||||
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
let instance_expected_field_type;
|
||||
if (
|
||||
protocolName &&
|
||||
this.protocols &&
|
||||
this.protocols[protocolName] &&
|
||||
this.protocols[protocolName].location_fields.length > 0 &&
|
||||
this.protocols[protocolName].field_types
|
||||
) {
|
||||
const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
||||
}
|
||||
|
||||
|
||||
let placeholder = _t('Search for a room');
|
||||
if (!this.state.instanceId) {
|
||||
placeholder = _t('#example') + ':' + this.state.roomServer;
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
}
|
||||
|
||||
let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
|
||||
if (protocolName) {
|
||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
|
||||
showJoinButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
return (
|
||||
<div className="mx_RoomDirectory">
|
||||
<SimpleRoomHeader title={ _t('Directory') } icon="img/icons-directory.svg" />
|
||||
<div className="mx_RoomDirectory_list">
|
||||
<div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function get_display_alias_for_room(room) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,66 +16,87 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
|
||||
const HIDE_DEBOUNCE_MS = 10000;
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||
|
||||
function getUnsentMessages(room) {
|
||||
if (!room) { return []; }
|
||||
return room.getPendingEvents().filter(function(ev) {
|
||||
return ev.status === Matrix.EventStatus.NOT_SENT;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomStatusBar',
|
||||
|
||||
propTypes: {
|
||||
// the room this statusbar is representing.
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
|
||||
// string to display when there are messages in the room which had errors on send
|
||||
unsentMessageError: React.PropTypes.string,
|
||||
numUnreadMessages: PropTypes.number,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline.
|
||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||
atEndOfLiveTimeline: PropTypes.bool,
|
||||
|
||||
// This is true when the user is alone in the room, but has also sent a message.
|
||||
// Used to suggest to the user to invite someone
|
||||
sentMessageAndIsAlone: PropTypes.bool,
|
||||
|
||||
// true if there is an active call in this room (means we show
|
||||
// the 'Active Call' text in the status bar if there is nothing
|
||||
// more interesting)
|
||||
hasActiveCall: React.PropTypes.bool,
|
||||
hasActiveCall: PropTypes.bool,
|
||||
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: React.PropTypes.number,
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: React.PropTypes.func,
|
||||
onResendAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'cancel all' button in the
|
||||
// 'unsent messages' bar
|
||||
onCancelAllClick: React.PropTypes.func,
|
||||
onCancelAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'invite others' button in the
|
||||
// 'you are alone' bar
|
||||
onInviteClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'stop warning me' button in the
|
||||
// 'you are alone' bar
|
||||
onStopWarningClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'scroll to bottom' button
|
||||
onScrollToBottomClick: React.PropTypes.func,
|
||||
onScrollToBottomClick: PropTypes.func,
|
||||
|
||||
// callback for when we do something that changes the size of the
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// component.
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
onVisible: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -86,13 +108,16 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
|
||||
this._checkSize();
|
||||
},
|
||||
|
@ -103,32 +128,65 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillUnmount: function() {
|
||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("sync", this.onSyncStateChange);
|
||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
onSyncStateChange: function(state, prevState) {
|
||||
onSyncStateChange: function(state, prevState, data) {
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
syncState: state
|
||||
syncState: state,
|
||||
syncStateData: data,
|
||||
});
|
||||
},
|
||||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
});
|
||||
},
|
||||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
},
|
||||
|
||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||
if (room.roomId !== this.props.room.roomId) return;
|
||||
|
||||
this.setState({
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||
_checkSize: function () {
|
||||
if (this.props.onVisible && this._getSize()) {
|
||||
this.props.onVisible();
|
||||
_checkSize: function() {
|
||||
if (this._getSize()) {
|
||||
if (this.props.onVisible) this.props.onVisible();
|
||||
} else {
|
||||
if (this.props.onHidden) this.props.onHidden();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -136,14 +194,15 @@ module.exports = React.createClass({
|
|||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function() {
|
||||
if (this.state.syncState === "ERROR" ||
|
||||
if (this._shouldShowConnectionError() ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall
|
||||
this.props.hasActiveCall ||
|
||||
this.props.sentMessageAndIsAlone
|
||||
) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.props.unsentMessageError) {
|
||||
} else if (this.state.unsentMessages.length > 0) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
|
@ -157,9 +216,9 @@ module.exports = React.createClass({
|
|||
if (this.props.numUnreadMessages) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/newmessages.svg" width="24" height="24"
|
||||
alt=""/>
|
||||
alt="" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -167,29 +226,29 @@ module.exports = React.createClass({
|
|||
if (!this.props.atEndOfLiveTimeline) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/scrolldown.svg" width="24" height="24"
|
||||
alt={ _t("Scroll to bottom of page") }
|
||||
title={ _t("Scroll to bottom of page") }/>
|
||||
alt={_t("Scroll to bottom of page")}
|
||||
title={_t("Scroll to bottom of page")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.hasActiveCall) {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return (
|
||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20"/>
|
||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20" />
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.syncState === "ERROR") {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wantPlaceholder) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
||||
{ this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -221,78 +280,169 @@ module.exports = React.createClass({
|
|||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||
+{othersCount}
|
||||
</span>
|
||||
+{ othersCount }
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return avatars;
|
||||
},
|
||||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
_shouldShowConnectionError: function() {
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
// without a connection! (technically may already have some but meh)
|
||||
// It also trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
if (this.state.syncState === "ERROR") {
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a resource limit exceeded error: those are shown in the top bar.
|
||||
const errorIsMauError = Boolean(
|
||||
this.state.syncStateData &&
|
||||
this.state.syncStateData.error &&
|
||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
|
||||
);
|
||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||
},
|
||||
|
||||
_getUnsentMessageContent: function() {
|
||||
const unsentMessages = this.state.unsentMessages;
|
||||
if (!unsentMessages.length) return null;
|
||||
|
||||
let title;
|
||||
let content;
|
||||
|
||||
const hasUDE = unsentMessages.some((m) => {
|
||||
return m.error && m.error.name === "UnknownDeviceError";
|
||||
});
|
||||
|
||||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown devices being present");
|
||||
content = _t(
|
||||
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
|
||||
{},
|
||||
{
|
||||
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
|
||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let consentError = null;
|
||||
let resourceLimitError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
unsentMessages[0].error.data.error
|
||||
) {
|
||||
title = unsentMessages[0].error.data.error;
|
||||
} else {
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||
"You can also select individual messages to resend or cancel.",
|
||||
{ count: unsentMessages.length },
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
'cancelText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{_t('Connectivity to the server has been lost.')}
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{_t('Sent messages will be stored until your connection has returned.')}
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.unsentMessageError) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ this.props.unsentMessageError }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (this.state.unsentMessages.length > 0) {
|
||||
return this._getUnsentMessageContent();
|
||||
}
|
||||
|
||||
// unread count trumps who is typing since the unread count is only
|
||||
// set when you've scrolled up
|
||||
if (this.props.numUnreadMessages) {
|
||||
// MUST use var name "count" for pluralization to kick in
|
||||
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||
const unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
{unreadMsgs}
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
{ unreadMsgs }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
this.state.usersTyping,
|
||||
this.props.whoIsTypingLimit
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
if (typingString) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingBar">
|
||||
<EmojiText>{typingString}</EmojiText>
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -300,7 +450,25 @@ module.exports = React.createClass({
|
|||
if (this.props.hasActiveCall) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_callBar">
|
||||
<b>{_t('Active call')}</b>
|
||||
<b>{ _t('Active call') }</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||
if (this.props.sentMessageAndIsAlone) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
{},
|
||||
{
|
||||
'inviteText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||
'nowarnText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -308,17 +476,16 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
},
|
||||
|
||||
|
||||
render: function() {
|
||||
var content = this._getContent();
|
||||
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
const content = this._getContent();
|
||||
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
<div className="mx_RoomStatusBar_indicator">
|
||||
{indicator}
|
||||
{ indicator }
|
||||
</div>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
469
src/components/structures/RoomSubList.js
Normal file
469
src/components/structures/RoomSubList.js
Normal file
|
@ -0,0 +1,469 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
||||
const TRUNCATE_AT = 10;
|
||||
|
||||
const RoomSubList = React.createClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
startAsHidden: PropTypes.bool,
|
||||
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
alwaysShowHeader: PropTypes.bool,
|
||||
incomingCall: PropTypes.object,
|
||||
onShowMoreRooms: PropTypes.func,
|
||||
searchFilter: PropTypes.string,
|
||||
emptyContent: PropTypes.node, // content shown if the list is empty
|
||||
headerItems: PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
showEmpty: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hidden: this.props.startAsHidden || false,
|
||||
truncateAt: TRUNCATE_AT,
|
||||
sortedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
onShowMoreRooms: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
showEmpty: true,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.setState({
|
||||
sortedList: this.applySearchFilter(this.props.list, this.props.searchFilter),
|
||||
});
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// order the room list appropriately before we re-render
|
||||
//if (debug) console.log("received new props, list = " + newProps.list);
|
||||
this.setState({
|
||||
sortedList: this.applySearchFilter(newProps.list, newProps.searchFilter),
|
||||
});
|
||||
},
|
||||
|
||||
applySearchFilter: function(list, filter) {
|
||||
if (filter === "") return list;
|
||||
const lcFilter = filter.toLowerCase();
|
||||
// case insensitive if room name includes filter,
|
||||
// or if starts with `#` and one of room's aliases starts with filter
|
||||
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
},
|
||||
|
||||
// The header is collapsable if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
//
|
||||
// Ultimately we need to transition to a state pushing flow where something
|
||||
// explicitly notifies the components concerned that the notif count for a room
|
||||
// has change (e.g. a Flux store).
|
||||
if (payload.action === 'on_room_read' &&
|
||||
this.props.list.some((r) => r.roomId === payload.roomId)
|
||||
) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden});
|
||||
|
||||
if (isHidden) {
|
||||
// as good a way as any to reset the truncate state
|
||||
this.setState({truncateAt: TRUNCATE_AT});
|
||||
}
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
this.props.onHeaderClick(isHidden);
|
||||
} else {
|
||||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
||||
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
_shouldShowNotifBadge: function(roomNotifState) {
|
||||
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
||||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function(truncateAt) {
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
}
|
||||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
if (badges) {
|
||||
result[0] += notificationCount;
|
||||
if (highlight) {
|
||||
result[1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [0, false]);
|
||||
},
|
||||
|
||||
_updateSubListCount: function() {
|
||||
// Force an update by setting the state to the current state
|
||||
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
||||
// method is honoured
|
||||
this.setState(this.state);
|
||||
},
|
||||
|
||||
makeRoomTiles: function() {
|
||||
const DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
return this.state.sortedList.map((room, index) => {
|
||||
// XXX: is it evil to pass in this as a prop to RoomTile? Yes.
|
||||
|
||||
// We should only use <DNDRoomTile /> when editable
|
||||
const RoomTileComponent = this.props.editable ? DNDRoomTile : RoomTile;
|
||||
return <RoomTileComponent
|
||||
index={index} // For DND
|
||||
room={room}
|
||||
roomSubList={this}
|
||||
tagName={this.props.tagName}
|
||||
key={room.roomId}
|
||||
collapsed={this.props.collapsed || false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
|
||||
isInvite={this.props.isInvite}
|
||||
refreshSubList={this._updateSubListCount}
|
||||
incomingCall={null}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.state.sortedList) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
if (notifBadges || mentionBadges) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// switch to first room in sortedList as that'll be the top of the list for the user
|
||||
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.sortedList[0].roomId,
|
||||
});
|
||||
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
||||
// Group Invites are different in that they are all extra tiles and not rooms
|
||||
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
||||
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.extraTiles[0].props.group.groupId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
const subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
const totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
const roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
|
||||
let badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
// the full tag name and room count
|
||||
let title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
title += " [" + roomCount + "]";
|
||||
}
|
||||
}
|
||||
|
||||
let incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
const self = this;
|
||||
// Check if the incoming call is for this section
|
||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
}
|
||||
|
||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||
<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} />
|
||||
{badge}
|
||||
{incomingCall}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
let content = <div className="mx_RoomSubList_chevronDown" />;
|
||||
|
||||
const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
const overflowNotifCount = overflowNotifications[0];
|
||||
const overflowNotifHighlight = overflowNotifications[1];
|
||||
if (overflowNotifCount && !this.props.collapsed) {
|
||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||
}
|
||||
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_moreBadge': true,
|
||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line" />
|
||||
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
||||
<div className={badgeClasses}>{content}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
this.props.onHeaderClick(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
let content;
|
||||
|
||||
if (this.props.showEmpty) {
|
||||
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
||||
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
||||
if (!this.props.searchFilter && this.props.emptyContent) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
// don't show an empty sublist
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
let subList;
|
||||
const classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{content}
|
||||
</TruncatedList>;
|
||||
} else {
|
||||
subList = <TruncatedList className={classes}>
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
const subListContent = <div>
|
||||
{this._getHeaderJsx()}
|
||||
{subList}
|
||||
</div>;
|
||||
|
||||
return this.props.editable ?
|
||||
<Droppable
|
||||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||
type="draggable-RoomTile"
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}>
|
||||
{subListContent}
|
||||
</div>
|
||||
)}
|
||||
</Droppable> : subListContent;
|
||||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.props.showSpinner) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||
{ this.state.hidden ? undefined : content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
File diff suppressed because it is too large
Load diff
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
var KeyCode = require('../../KeyCode');
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index.js';
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
const DEBUG_SCROLL = false;
|
||||
// var DEBUG_SCROLL = true;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
|
@ -86,7 +87,7 @@ module.exports = React.createClass({
|
|||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
|
@ -95,7 +96,7 @@ module.exports = React.createClass({
|
|||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom: React.PropTypes.bool,
|
||||
startAtBottom: PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
|
@ -110,7 +111,7 @@ module.exports = React.createClass({
|
|||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest: React.PropTypes.func,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
|
@ -121,24 +122,24 @@ module.exports = React.createClass({
|
|||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: React.PropTypes.func,
|
||||
onUnfillRequest: PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onResize: a callback which is called whenever the Gemini scroll
|
||||
* panel is resized
|
||||
*/
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: React.PropTypes.object,
|
||||
style: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -148,6 +149,7 @@ module.exports = React.createClass({
|
|||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onScroll: function() {},
|
||||
onResize: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -178,7 +180,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onScroll: function(ev) {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
debuglog("Scroll event: offset now:", sn.scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
|
||||
|
@ -222,7 +224,7 @@ module.exports = React.createClass({
|
|||
onResize: function() {
|
||||
this.props.onResize();
|
||||
this.checkScroll();
|
||||
this.refs.geminiPanel.forceUpdate();
|
||||
if (this._gemScroll) this._gemScroll.forceUpdate();
|
||||
},
|
||||
|
||||
// after an update to the contents of the panel, check that the scroll is
|
||||
|
@ -238,7 +240,7 @@ module.exports = React.createClass({
|
|||
// about whether the the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom: function() {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
|
||||
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
||||
// understanding of the box model, wherein the scrollNode ends up 2
|
||||
|
@ -281,7 +283,7 @@ module.exports = React.createClass({
|
|||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight: function(backwards) {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
if (backwards) {
|
||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
} else {
|
||||
|
@ -295,7 +297,7 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
// viewport, try to get some more messages.
|
||||
|
@ -377,7 +379,7 @@ module.exports = React.createClass({
|
|||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill: function(backwards) {
|
||||
var dir = backwards ? 'b' : 'f';
|
||||
const dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||
return;
|
||||
|
@ -470,8 +472,8 @@ module.exports = React.createClass({
|
|||
* mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
var delta = mult * scrollNode.clientHeight * 0.5;
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
this._setScrollTop(scrollNode.scrollTop + delta);
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
@ -535,7 +537,7 @@ module.exports = React.createClass({
|
|||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: scrollToken,
|
||||
pixelOffset: pixelOffset
|
||||
pixelOffset: pixelOffset,
|
||||
};
|
||||
|
||||
// ... then make it so.
|
||||
|
@ -546,10 +548,10 @@ module.exports = React.createClass({
|
|||
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||
/* find the dom node with the right scrolltoken */
|
||||
var node;
|
||||
var messages = this.refs.itemlist.children;
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var m = messages[i];
|
||||
let node;
|
||||
const messages = this.refs.itemlist.children;
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const m = messages[i];
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
// There might only be one scroll token
|
||||
if (m.dataset.scrollTokens &&
|
||||
|
@ -564,18 +566,17 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var scrollNode = this._getScrollNode();
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
const scrollNode = this._getScrollNode();
|
||||
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
|
||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||
pixelOffset + " (delta: "+scrollDelta+")");
|
||||
|
||||
if(scrollDelta != 0) {
|
||||
if (scrollDelta != 0) {
|
||||
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
_saveScrollState: function() {
|
||||
|
@ -585,16 +586,16 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var itemlist = this.refs.itemlist;
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var messages = itemlist.children;
|
||||
const itemlist = this.refs.itemlist;
|
||||
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
const messages = itemlist.children;
|
||||
let newScrollState = null;
|
||||
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var node = messages[i];
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const node = messages[i];
|
||||
if (!node.dataset.scrollTokens) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
newScrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||
|
@ -619,8 +620,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
var scrollState = this.scrollState;
|
||||
var scrollNode = this._getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const scrollNode = this._getScrollNode();
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
this._setScrollTop(Number.MAX_VALUE);
|
||||
|
@ -631,9 +632,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_setScrollTop: function(scrollTop) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
const scrollNode = this._getScrollNode();
|
||||
|
||||
var prevScroll = scrollNode.scrollTop;
|
||||
const prevScroll = scrollNode.scrollTop;
|
||||
|
||||
// FF ignores attempts to set scrollTop to very large numbers
|
||||
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
|
||||
|
@ -664,22 +665,33 @@ module.exports = React.createClass({
|
|||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
return this.refs.geminiPanel.scrollbar.getViewElement();
|
||||
if (!this._gemScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
|
||||
}
|
||||
|
||||
return this._gemScroll.scrollbar.getViewElement();
|
||||
},
|
||||
|
||||
_collectGeminiScroll: function(gemScroll) {
|
||||
this._gemScroll = gemScroll;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
// TODO: the classnames on the div and ol could do with being updated to
|
||||
// reflect the fact that we don't necessarily contain a list of messages.
|
||||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
return (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{this.props.children}
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
163
src/components/structures/SearchBox.js
Normal file
163
src/components/structures/SearchBox.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import rate_limited_func from '../../ratelimitedfunc';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SearchBox',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool,
|
||||
onSearch: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
searchTerm: "",
|
||||
};
|
||||
},
|
||||
|
||||
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 });
|
||||
this.onSearch();
|
||||
},
|
||||
|
||||
onSearch: new rate_limited_func(
|
||||
function() {
|
||||
this.props.onSearch(this.refs.search.value);
|
||||
},
|
||||
100
|
||||
),
|
||||
|
||||
onToggleCollapse: function(show) {
|
||||
if (show) {
|
||||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_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 =
|
||||
<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 =
|
||||
<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 ?
|
||||
<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ ()=>{ this._clearSearch(); } }>
|
||||
<TintableSvg
|
||||
className="mx_SearchBox_searchButton"
|
||||
src="img/icons-close.svg" width="24" height="24"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
:
|
||||
<TintableSvg
|
||||
key="button"
|
||||
className="mx_SearchBox_searchButton"
|
||||
src="img/icons-search-copy.svg" width="13" height="13"
|
||||
/>,
|
||||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
ref="search"
|
||||
className="mx_SearchBox_search"
|
||||
value={ this.state.searchTerm }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox">
|
||||
{ searchControls }
|
||||
{ toggleCollapse }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
170
src/components/structures/TagPanel.js
Normal file
170
src/components/structures/TagPanel.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
orderedTags: [],
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||
selectedTags: TagOrderStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.removeListener("sync", this._onClientSync);
|
||||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership() {
|
||||
if (this.unmounted) return;
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
},
|
||||
|
||||
_onClientSync(syncState, prevState) {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
// Load joined groups
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
// only dispatch if its not a no-op
|
||||
if (this.state.selectedTags.length > 0) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
}
|
||||
},
|
||||
|
||||
onCreateGroupClick(ev) {
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
onClearFilterClick(ev) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
key={tag}
|
||||
tag={tag}
|
||||
index={index}
|
||||
selected={this.state.selectedTags.includes(tag)}
|
||||
/>;
|
||||
});
|
||||
|
||||
const itemsSelected = this.state.selectedTags.length > 0;
|
||||
|
||||
let clearButton;
|
||||
if (itemsSelected) {
|
||||
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_TagPanel', {
|
||||
mx_TagPanel_items_selected: itemsSelected,
|
||||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_TagPanel_clearButton_container">
|
||||
{ clearButton }
|
||||
</div>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
className="mx_TagPanel_scroller"
|
||||
autoshow={true}
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ tags }
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbarWrapper>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_groupsButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
export default TagPanel;
|
|
@ -15,27 +15,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var EventTimeline = Matrix.EventTimeline;
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const EventTimeline = Matrix.EventTimeline;
|
||||
|
||||
var sdk = require('../../index');
|
||||
const sdk = require('../../index');
|
||||
import { _t } from '../../languageHandler';
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var dis = require("../../dispatcher");
|
||||
var ObjectUtils = require('../../ObjectUtils');
|
||||
var Modal = require("../../Modal");
|
||||
var UserActivity = require("../../UserActivity");
|
||||
var KeyCode = require('../../KeyCode');
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const Modal = require("../../Modal");
|
||||
const UserActivity = require("../../UserActivity");
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
|
||||
var PAGINATE_SIZE = 20;
|
||||
var INITIAL_SIZE = 20;
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
|
||||
var DEBUG = false;
|
||||
const DEBUG = false;
|
||||
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
|
@ -57,52 +59,49 @@ var TimelinePanel = React.createClass({
|
|||
// representing. This may or may not have a room, depending on what it's
|
||||
// a timeline representing. If it has a room, we maintain RRs etc for
|
||||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
timelineSet: PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
manageReadMarkers: React.PropTypes.bool,
|
||||
manageReadReceipts: PropTypes.bool,
|
||||
manageReadMarkers: PropTypes.bool,
|
||||
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: React.PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
// typically this will be either 'eventId' or undefined.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// id of an event to jump to. If not given, will go to the end of the
|
||||
// live timeline.
|
||||
eventId: React.PropTypes.string,
|
||||
eventId: PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
// bottom of the viewport. If not given, will try to put the event
|
||||
// half way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
eventPixelOffset: PropTypes.number,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
onReadMarkerUpdated: PropTypes.func,
|
||||
|
||||
// maximum number of events to show in a timeline
|
||||
timelineCap: React.PropTypes.number,
|
||||
timelineCap: PropTypes.number,
|
||||
|
||||
// classname to use for the messagepanel
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// shape property to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: React.PropTypes.string,
|
||||
empty: PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -132,8 +131,6 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
return {
|
||||
events: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
|
@ -178,10 +175,10 @@ var TimelinePanel = React.createClass({
|
|||
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
||||
|
||||
// should the event tiles have twelve hour times
|
||||
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
|
||||
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
|
||||
// always show timestamps on event tiles?
|
||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -260,7 +257,7 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||
|
@ -275,20 +272,20 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
||||
// this particular event should be the first or last to be unpaginated.
|
||||
let eventId = scrollToken;
|
||||
const eventId = scrollToken;
|
||||
|
||||
let marker = this.state.events.findIndex(
|
||||
const marker = this.state.events.findIndex(
|
||||
(ev) => {
|
||||
return ev.getId() === eventId;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||
const count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||
|
||||
if (count > 0) {
|
||||
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
||||
|
@ -305,16 +302,18 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// set off a pagination request.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
|
||||
if (!this.state[canPaginateKey]) {
|
||||
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if(!this._timelineWindow.canPaginate(dir)) {
|
||||
if (!this._timelineWindow.canPaginate(dir)) {
|
||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||
this.setState({[canPaginateKey]: false});
|
||||
return Promise.resolve(false);
|
||||
|
@ -328,7 +327,7 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||
|
||||
var newState = {
|
||||
const newState = {
|
||||
[paginatingKey]: false,
|
||||
[canPaginateKey]: r,
|
||||
events: this._getEvents(),
|
||||
|
@ -336,8 +335,8 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// moving the window in this direction may mean that we can now
|
||||
// paginate in the other where we previously could not.
|
||||
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||
if (!this.state[canPaginateOtherWayKey] &&
|
||||
this._timelineWindow.canPaginate(otherDirection)) {
|
||||
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
||||
|
@ -384,6 +383,9 @@ var TimelinePanel = React.createClass({
|
|||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
break;
|
||||
case 'ignore_state_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -417,15 +419,15 @@ var TimelinePanel = React.createClass({
|
|||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
||||
if (this.unmounted) { return; }
|
||||
|
||||
var events = this._timelineWindow.getEvents();
|
||||
var lastEv = events[events.length-1];
|
||||
const events = this._timelineWindow.getEvents();
|
||||
const lastEv = events[events.length-1];
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
events.push(...this.props.timelineSet.room.getPendingEvents());
|
||||
}
|
||||
|
||||
var updatedState = {events: events};
|
||||
const updatedState = {events: events};
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
// when a new event arrives when the user is not watching the
|
||||
|
@ -436,12 +438,12 @@ var TimelinePanel = React.createClass({
|
|||
// read-marker when a remote echo of an event we have just sent takes
|
||||
// more than the timeout on userCurrentlyActive.
|
||||
//
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var sender = ev.sender ? ev.sender.userId : null;
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const sender = ev.sender ? ev.sender.userId : null;
|
||||
var callback = null;
|
||||
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
||||
updatedState.readMarkerVisible = true;
|
||||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||
} else if (lastEv && this.getReadMarkerPosition() === 0) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
|
@ -622,6 +624,7 @@ var TimelinePanel = React.createClass({
|
|||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||
dis.dispatch({
|
||||
action: 'on_room_read',
|
||||
roomId: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -643,7 +646,7 @@ var TimelinePanel = React.createClass({
|
|||
// and we'll get confused when their ID changes and we can't figure out
|
||||
// where the RM is pointing to. The read marker will be invisible for
|
||||
// now anyway, so this doesn't really matter.
|
||||
var lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
const lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
allowPartial: true,
|
||||
ignoreEchoes: true,
|
||||
});
|
||||
|
@ -652,13 +655,13 @@ var TimelinePanel = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs());
|
||||
|
||||
// the read-marker should become invisible, so that if the user scrolls
|
||||
// down, they don't see it.
|
||||
if(this.state.readMarkerVisible) {
|
||||
if (this.state.readMarkerVisible) {
|
||||
this.setState({
|
||||
readMarkerVisible: false,
|
||||
});
|
||||
|
@ -673,7 +676,7 @@ var TimelinePanel = React.createClass({
|
|||
// we call _timelineWindow.getEvents() rather than using
|
||||
// this.state.events, because react batches the update to the latter, so it
|
||||
// may not have been updated yet.
|
||||
var events = this._timelineWindow.getEvents();
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// first find where the current RM is
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
|
@ -686,7 +689,7 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
|
||||
// now think about advancing it
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
for (i++; i < events.length; i++) {
|
||||
var ev = events[i];
|
||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
||||
|
@ -731,7 +734,7 @@ var TimelinePanel = React.createClass({
|
|||
//
|
||||
// a quick way to figure out if we've loaded the relevant event is
|
||||
// simply to check if the messagepanel knows where the read-marker is.
|
||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
// The messagepanel knows where the RM is, so we must have loaded
|
||||
// the relevant event.
|
||||
|
@ -752,13 +755,13 @@ var TimelinePanel = React.createClass({
|
|||
forgetReadMarker: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
|
||||
var rmId = this._getCurrentReadReceipt();
|
||||
const rmId = this._getCurrentReadReceipt();
|
||||
|
||||
// see if we know the timestamp for the rr event
|
||||
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||
var rmTs;
|
||||
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||
let rmTs;
|
||||
if (tl) {
|
||||
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||
const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||
if (event) {
|
||||
rmTs = event.getTs();
|
||||
}
|
||||
|
@ -798,7 +801,7 @@ var TimelinePanel = React.createClass({
|
|||
if (!this.props.manageReadMarkers) return null;
|
||||
if (!this.refs.messagePanel) return null;
|
||||
|
||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
return ret;
|
||||
}
|
||||
|
@ -841,8 +844,7 @@ var TimelinePanel = React.createClass({
|
|||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END)
|
||||
{
|
||||
ev.keyCode == KeyCode.END) {
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
|
@ -850,12 +852,12 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
_initTimeline: function(props) {
|
||||
var initialEvent = props.eventId;
|
||||
var pixelOffset = props.eventPixelOffset;
|
||||
const initialEvent = props.eventId;
|
||||
const pixelOffset = props.eventPixelOffset;
|
||||
|
||||
// if a pixelOffset is given, it is relative to the bottom of the
|
||||
// container. If not, put the event in the middle of the container.
|
||||
var offsetBase = 1;
|
||||
let offsetBase = 1;
|
||||
if (pixelOffset == null) {
|
||||
offsetBase = 0.5;
|
||||
}
|
||||
|
@ -884,7 +886,7 @@ var TimelinePanel = React.createClass({
|
|||
MatrixClientPeg.get(), this.props.timelineSet,
|
||||
{windowLimit: this.props.timelineCap});
|
||||
|
||||
var onLoaded = () => {
|
||||
const onLoaded = () => {
|
||||
this._reloadEvents();
|
||||
|
||||
// If we switched away from the room while there were pending
|
||||
|
@ -919,15 +921,15 @@ var TimelinePanel = React.createClass({
|
|||
});
|
||||
};
|
||||
|
||||
var onError = (error) => {
|
||||
const onError = (error) => {
|
||||
this.setState({timelineLoading: false});
|
||||
console.error(
|
||||
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||
);
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const msg = error.message ? error.message : JSON.stringify(error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
var onFinished;
|
||||
let onFinished;
|
||||
|
||||
// if we were given an event ID, then when the user closes the
|
||||
// dialog, let's jump to the end of the timeline. If we weren't,
|
||||
|
@ -942,7 +944,7 @@ var TimelinePanel = React.createClass({
|
|||
});
|
||||
};
|
||||
}
|
||||
var message = (error.errcode == 'M_FORBIDDEN')
|
||||
const message = (error.errcode == 'M_FORBIDDEN')
|
||||
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
||||
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||
|
@ -952,7 +954,7 @@ var TimelinePanel = React.createClass({
|
|||
});
|
||||
};
|
||||
|
||||
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
|
||||
// if we already have the event in question, TimelineWindow.load
|
||||
// returns a resolved promise.
|
||||
|
@ -993,7 +995,7 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// get the list of events from the timeline window and the pending event list
|
||||
_getEvents: function() {
|
||||
var events = this._timelineWindow.getEvents();
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
|
@ -1004,7 +1006,7 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
_indexForEventId: function(evId) {
|
||||
for (var i = 0; i < this.state.events.length; ++i) {
|
||||
for (let i = 0; i < this.state.events.length; ++i) {
|
||||
if (evId == this.state.events[i].getId()) {
|
||||
return i;
|
||||
}
|
||||
|
@ -1014,18 +1016,18 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
_getLastDisplayedEventIndex: function(opts) {
|
||||
opts = opts || {};
|
||||
var ignoreOwn = opts.ignoreOwn || false;
|
||||
var ignoreEchoes = opts.ignoreEchoes || false;
|
||||
var allowPartial = opts.allowPartial || false;
|
||||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const ignoreEchoes = opts.ignoreEchoes || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
var messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||
var ev = this.state.events[i];
|
||||
for (let i = this.state.events.length-1; i >= 0; --i) {
|
||||
const ev = this.state.events[i];
|
||||
|
||||
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
||||
continue;
|
||||
|
@ -1036,10 +1038,10 @@ var TimelinePanel = React.createClass({
|
|||
continue;
|
||||
}
|
||||
|
||||
var node = messagePanel.getNodeForEventId(ev.getId());
|
||||
const node = messagePanel.getNodeForEventId(ev.getId());
|
||||
if (!node) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||
return i;
|
||||
|
@ -1057,18 +1059,18 @@ var TimelinePanel = React.createClass({
|
|||
* SDK.
|
||||
*/
|
||||
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
// the client can be null on logout
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var myUserId = client.credentials.userId;
|
||||
const myUserId = client.credentials.userId;
|
||||
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
},
|
||||
|
||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
var roomId = this.props.timelineSet.room.roomId;
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
|
@ -1092,9 +1094,20 @@ var TimelinePanel = React.createClass({
|
|||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
_shouldPaginate: function() {
|
||||
// don't try to paginate while events in the timeline are
|
||||
// still being decrypted. We don't render events while they're
|
||||
// being decrypted, so they don't take up space in the timeline.
|
||||
// This means we can pull quite a lot of events into the timeline
|
||||
// and end up trying to render a lot of events.
|
||||
return !this.state.events.some((e) => {
|
||||
return e.isBeingDecrypted();
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
// just show a spinner while the timeline loads.
|
||||
//
|
||||
|
@ -1109,15 +1122,15 @@ var TimelinePanel = React.createClass({
|
|||
// exist.
|
||||
if (this.state.timelineLoading) {
|
||||
return (
|
||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||
<Loader />
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanelSpinner">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||
return (
|
||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1131,35 +1144,35 @@ var TimelinePanel = React.createClass({
|
|||
// forwards, otherwise if somebody hits the bottom of the loaded
|
||||
// events when viewing historical messages, we get stuck in a loop
|
||||
// of paginating our way through the entire history of the room.
|
||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
|
||||
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
||||
// If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with
|
||||
// the HS and fetch the latest events, so we are effectively forward paginating.
|
||||
const forwardPaginating = (
|
||||
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
|
||||
this.state.forwardPaginating ||
|
||||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
hidden={ this.props.hidden }
|
||||
backPaginating={ this.state.backPaginating }
|
||||
forwardPaginating={ forwardPaginating }
|
||||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview={ this.props.showUrlPreview }
|
||||
showReadReceipts={ this.props.showReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||
opacity={ this.props.opacity }
|
||||
isTwelveHour={ this.state.isTwelveHour }
|
||||
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
|
||||
className={ this.props.className }
|
||||
tileShape={ this.props.tileShape }
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
events={this.state.events}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
readMarkerEventId={this.state.readMarkerEventId}
|
||||
readMarkerVisible={this.state.readMarkerVisible}
|
||||
suppressFirstDateSeparator={this.state.canBackPaginate}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ContentMessages = require('../../ContentMessages');
|
||||
var dis = require('../../dispatcher');
|
||||
var filesize = require('filesize');
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ContentMessages = require('../../ContentMessages');
|
||||
const dis = require('../../dispatcher');
|
||||
const filesize = require('filesize');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: React.PropTypes.object
|
||||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -46,7 +47,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var uploads = ContentMessages.getCurrentUploads();
|
||||
const uploads = ContentMessages.getCurrentUploads();
|
||||
|
||||
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
|
||||
// check in RoomView
|
||||
|
@ -62,8 +63,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
return <div />;
|
||||
}
|
||||
|
||||
var upload;
|
||||
for (var i = 0; i < uploads.length; ++i) {
|
||||
let upload;
|
||||
for (let i = 0; i < uploads.length; ++i) {
|
||||
if (uploads[i].roomId == this.props.room.roomId) {
|
||||
upload = uploads[i];
|
||||
break;
|
||||
|
@ -73,32 +74,32 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
return <div />;
|
||||
}
|
||||
|
||||
var innerProgressStyle = {
|
||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
|
||||
const innerProgressStyle = {
|
||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
|
||||
};
|
||||
var uploadedSize = filesize(upload.loaded);
|
||||
var totalSize = filesize(upload.total);
|
||||
let uploadedSize = filesize(upload.loaded);
|
||||
const totalSize = filesize(upload.total);
|
||||
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
|
||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||
}
|
||||
|
||||
// MUST use var name 'count' for pluralization to kick in
|
||||
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||
const uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||
|
||||
return (
|
||||
<div className="mx_UploadBar">
|
||||
<div className="mx_UploadBar_uploadProgressOuter">
|
||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||
</div>
|
||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22"/>
|
||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22" />
|
||||
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||
/>
|
||||
<div className="mx_UploadBar_uploadBytes">
|
||||
{ uploadedSize } / { totalSize }
|
||||
</div>
|
||||
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
|
||||
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
57
src/components/structures/ViewSource.js
Normal file
57
src/components/structures/ViewSource.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ViewSource',
|
||||
|
||||
propTypes: {
|
||||
content: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode == 27) { // escape
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_ViewSource">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.content, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,50 +15,52 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var sdk = require('../../../index');
|
||||
var Modal = require("../../../Modal");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
var PasswordReset = require("../../../PasswordReset");
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
onLoginClick: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func,
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
onLoginClick: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
progress: null
|
||||
progress: null,
|
||||
password: null,
|
||||
password2: null,
|
||||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
this.setState({
|
||||
progress: "sending_email"
|
||||
progress: "sending_email",
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.setState({
|
||||
progress: "sent_email"
|
||||
progress: "sent_email",
|
||||
});
|
||||
}, (err) => {
|
||||
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||
this.setState({
|
||||
progress: null
|
||||
progress: null,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -81,15 +83,12 @@ module.exports = React.createClass({
|
|||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
}
|
||||
else if (!this.state.password || !this.state.password2) {
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
this.showErrorDialog(_t('A new password must be entered.'));
|
||||
}
|
||||
else if (this.state.password !== this.state.password2) {
|
||||
} else if (this.state.password !== this.state.password2) {
|
||||
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||
}
|
||||
else {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
|
@ -99,21 +98,21 @@ module.exports = React.createClass({
|
|||
'end-to-end encryption keys on all devices, ' +
|
||||
'making encrypted chat history unreadable, ' +
|
||||
'unless you first export your room keys and re-import ' +
|
||||
'them afterwards. In future this will be improved.'
|
||||
'them afterwards. In future this will be improved.',
|
||||
) }
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
<button key="export_keys" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
this.state.email, this.state.password,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -133,7 +132,7 @@ module.exports = React.createClass({
|
|||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
this.setState({
|
||||
[stateKey]: ev.target.value
|
||||
[stateKey]: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -149,7 +148,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||
title: title,
|
||||
description: body,
|
||||
|
@ -157,37 +156,53 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var resetPasswordJsx;
|
||||
let resetPasswordJsx;
|
||||
|
||||
if (this.state.progress === "sending_email") {
|
||||
resetPasswordJsx = <Spinner />;
|
||||
}
|
||||
else if (this.state.progress === "sent_email") {
|
||||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
{ _t('An email has been sent to') } {this.state.email}. { _t("Once you've followed the link it contains, click below") }.
|
||||
<div className="mx_Login_prompt">
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " +
|
||||
"click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={ _t('I have verified my email address') } />
|
||||
value={_t('I have verified my email address')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (this.state.progress === "complete") {
|
||||
} else if (this.state.progress === "complete") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
<p>{ _t('Your password has been reset') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. ' +
|
||||
'To re-enable notifications, sign in again on each device') }.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={ _t('Return to login screen') } />
|
||||
value={_t('Return to login screen')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
|
@ -199,38 +214,30 @@ module.exports = React.createClass({
|
|||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
placeholder={ _t('Email address') } autoFocus />
|
||||
placeholder={_t('Email address')} autoFocus />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password"
|
||||
value={this.state.password}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
placeholder={ _t('New password') } />
|
||||
placeholder={_t('New password')} />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password_confirm"
|
||||
value={this.state.password2}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
placeholder={ _t('Confirm your new password') } />
|
||||
placeholder={_t('Confirm your new password')} />
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0}/>
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
{ serverConfigSection }
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{_t('Return to login screen')}
|
||||
{ _t('Return to login screen') }
|
||||
</a>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -239,12 +246,12 @@ module.exports = React.createClass({
|
|||
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
{resetPasswordJsx}
|
||||
{ resetPasswordJsx }
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
38
src/components/structures/login/LanguageSelector.js
Normal file
38
src/components/structures/login/LanguageSelector.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getCurrentLanguage} from "../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
|
||||
function onChange(newLang) {
|
||||
if (getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector() {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={onChange} className="mx_Login_language" value={getCurrentLanguage()} />
|
||||
</div>;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,15 +19,16 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
|
@ -35,27 +37,28 @@ module.exports = React.createClass({
|
|||
displayName: 'Login',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
|
||||
enableGuest: React.PropTypes.bool,
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different home server without confusing users.
|
||||
fallbackHsUrl: React.PropTypes.string,
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -76,6 +79,14 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
// map from login step type to a function which will render a control
|
||||
// letting you do that login type
|
||||
this._stepRendererMap = {
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
'm.login.cas': this._renderCasStep,
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
|
@ -83,6 +94,13 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
loginIncorrect: Boolean(errorText),
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -95,17 +113,56 @@ module.exports = React.createClass({
|
|||
).then((data) => {
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
if(this._unmounted) {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
let errorText;
|
||||
|
||||
// Some error strings only apply for logging in
|
||||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus == 400 && usingEmail) {
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{errorTop}</div>
|
||||
<div className="mx_Login_smallError">{errorDetail}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
errorText = _t('Incorrect username and/or password.');
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
<div className="mx_Login_smallError">
|
||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
errorText = _t('Incorrect username and/or password.');
|
||||
}
|
||||
} else {
|
||||
// other errors, not specific to doing a password login
|
||||
errorText = this._errorTextFromError(error);
|
||||
|
@ -117,10 +174,10 @@ module.exports = React.createClass({
|
|||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
// mentions this (although the bug is for UI auth which is not this)
|
||||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if(this._unmounted) {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
|
@ -134,7 +191,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onLoginAsGuestClick: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
self.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
|
@ -156,7 +213,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
@ -183,8 +240,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
var self = this;
|
||||
let newState = {
|
||||
const self = this;
|
||||
const newState = {
|
||||
errorText: null, // reset err messages
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
|
@ -193,19 +250,21 @@ module.exports = React.createClass({
|
|||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
|
||||
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
this._loginLogic = loginLogic;
|
||||
|
@ -217,13 +276,29 @@ module.exports = React.createClass({
|
|||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
loginLogic.getFlows().then(function(flows) {
|
||||
// old behaviour was to always use the first flow without presenting
|
||||
// options. This works in most cases (we don't have a UI for multiple
|
||||
// logins so let's skip that for now).
|
||||
loginLogic.chooseFlow(0);
|
||||
self.setState({
|
||||
currentFlow: self._getCurrentFlowStep(),
|
||||
loginLogic.getFlows().then((flows) => {
|
||||
// look for a flow where we understand all of the steps.
|
||||
for (let i = 0; i < flows.length; i++ ) {
|
||||
if (!this._isSupportedFlow(flows[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we just pick the first flow where we support all the
|
||||
// steps. (we don't have a UI for multiple logins so let's skip
|
||||
// that for now).
|
||||
loginLogic.chooseFlow(i);
|
||||
this.setState({
|
||||
currentFlow: this._getCurrentFlowStep(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// we got to the end of the list without finding a suitable
|
||||
// flow.
|
||||
this.setState({
|
||||
errorText: _t(
|
||||
"This homeserver doesn't offer any login flows which are " +
|
||||
"supported by this client.",
|
||||
),
|
||||
});
|
||||
}, function(err) {
|
||||
self.setState({
|
||||
|
@ -237,6 +312,16 @@ module.exports = React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
_isSupportedFlow: function(flow) {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login and loginLogic doesn't support it so we can ignore it.
|
||||
if (!this._stepRendererMap[flow.type]) {
|
||||
console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_getCurrentFlowStep: function() {
|
||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||
},
|
||||
|
@ -256,18 +341,28 @@ module.exports = React.createClass({
|
|||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
|
||||
)}
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
|
||||
)}
|
||||
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " +
|
||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
@ -276,109 +371,114 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
case 'm.login.password':
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
default:
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
|
||||
</div>
|
||||
);
|
||||
if (!step) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepRenderer = this._stepRendererMap[step];
|
||||
|
||||
if (stepRenderer) {
|
||||
return stepRenderer();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
_onLanguageChange: function(newLang) {
|
||||
if(languageHandler.getCurrentLanguage() !== newLang) {
|
||||
UserSettingsStore.setLocalSetting('language', newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
_renderPasswordStep: function() {
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsUrl={this.state.enteredHomeserverUrl}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
||||
className="mx_Login_language"
|
||||
value={languageHandler.getCurrentLanguage()}
|
||||
/>
|
||||
</div>;
|
||||
_renderCasStep: function() {
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
let loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||
{ _t('Login as guest')}
|
||||
{ _t('Try the app first') }
|
||||
</a>;
|
||||
}
|
||||
|
||||
var returnToAppJsx;
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app')}
|
||||
</a>;
|
||||
let serverConfig;
|
||||
let header;
|
||||
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000} />;
|
||||
}
|
||||
|
||||
// FIXME: remove status.im theme tweaks
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
let errorTextSection;
|
||||
if (this.state.errorText) {
|
||||
errorTextSection = (
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div>
|
||||
<h2>{ _t('Sign in')}
|
||||
{ loader }
|
||||
</h2>
|
||||
{ header }
|
||||
{ errorTextSection }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
{ serverConfig }
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account')}
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
{ returnToAppJsx }
|
||||
{ this._renderLanguageSetting() }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -25,14 +26,14 @@ module.exports = React.createClass({
|
|||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
avatarUrl: null,
|
||||
errorString: null,
|
||||
busy: false
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -40,28 +41,29 @@ module.exports = React.createClass({
|
|||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||
// the URL to be passed to you (because it's also used for room avatars).
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
var self = this;
|
||||
const self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||
self.setState({
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: _t("Failed to fetch avatar URL"),
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div className="mx_Login_profile">
|
||||
|
@ -71,10 +73,10 @@ module.exports = React.createClass({
|
|||
<ChangeAvatar
|
||||
initialAvatarUrl={this.state.avatarUrl} />
|
||||
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
||||
{this.state.errorString}
|
||||
{ this.state.errorString }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,13 +20,16 @@ import Matrix from 'matrix-js-sdk';
|
|||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -33,31 +37,38 @@ module.exports = React.createClass({
|
|||
displayName: 'Registration',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
clientSecret: React.PropTypes.string,
|
||||
sessionId: React.PropTypes.string,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
idSid: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
brand: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
referrer: React.PropTypes.string,
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
clientSecret: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
teamServerConfig: PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string.isRequired,
|
||||
supportEmail: PropTypes.string.isRequired,
|
||||
// URL of the riot-team-server to get team configurations and track referrals
|
||||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
teamServerURL: PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
teamSelected: PropTypes.object,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onCancelClick: React.PropTypes.func
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
rtsClient: PropTypes.shape({
|
||||
getTeamsConfig: PropTypes.func.isRequired,
|
||||
trackReferral: PropTypes.func.isRequired,
|
||||
getTeam: PropTypes.func.isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -82,6 +93,7 @@ module.exports = React.createClass({
|
|||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
flows: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -121,23 +133,40 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
let newState = {};
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.hsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState, function() {
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, () => {
|
||||
this._replaceClient();
|
||||
});
|
||||
},
|
||||
|
||||
_replaceClient: function() {
|
||||
_replaceClient: async function() {
|
||||
this._matrixClient = Matrix.createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
idBaseUrl: this.state.isUrl,
|
||||
});
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
// auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 401) {
|
||||
this.setState({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods"),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
|
@ -153,12 +182,34 @@ module.exports = React.createClass({
|
|||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdn_available = false;
|
||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
msg = <div>
|
||||
<p>{errorTop}</p>
|
||||
<p>{errorDetail}</p>
|
||||
</div>;
|
||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdnAvailable = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
}
|
||||
if (!msisdn_available) {
|
||||
if (!msisdnAvailable) {
|
||||
msg = _t('This server does not support authentication with a phone number.');
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +246,7 @@ module.exports = React.createClass({
|
|||
|
||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
`User successfully registered with team ${team.name}`
|
||||
`User successfully registered with team ${team.name}`,
|
||||
);
|
||||
if (!team.rooms) {
|
||||
return;
|
||||
|
@ -223,7 +274,7 @@ module.exports = React.createClass({
|
|||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
accessToken: response.access_token,
|
||||
}, teamToken);
|
||||
}).then((cli) => {
|
||||
return this._setupPushers(cli);
|
||||
|
@ -237,7 +288,7 @@ module.exports = React.createClass({
|
|||
return matrixClient.getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
if (pushers[i].kind === 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
matrixClient.setPusher(emailPusher).done(() => {
|
||||
|
@ -253,7 +304,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFormValidationFailed: function(errCode) {
|
||||
var errMsg;
|
||||
let errMsg;
|
||||
switch (errCode) {
|
||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||
errMsg = _t('Missing password.');
|
||||
|
@ -262,7 +313,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t('Passwords don\'t match.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||
|
@ -282,7 +333,7 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
this.setState({
|
||||
errorText: errMsg
|
||||
errorText: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -316,16 +367,19 @@ module.exports = React.createClass({
|
|||
emailAddress: this.state.formVals.email,
|
||||
phoneCountry: this.state.formVals.phoneCountry,
|
||||
phoneNumber: this.state.formVals.phoneNumber,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
|
||||
let registerBody;
|
||||
if (this.state.doingUIAuth) {
|
||||
registerBody = (
|
||||
|
@ -341,12 +395,22 @@ module.exports = React.createClass({
|
|||
poll={true}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||
} else if (this.state.busy || this.state.teamServerBusy || !this.state.flows) {
|
||||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
);
|
||||
}
|
||||
registerBody = (
|
||||
<div>
|
||||
|
@ -361,31 +425,38 @@ module.exports = React.createClass({
|
|||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
flows={this.state.flows}
|
||||
/>
|
||||
{errorSection}
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
{ serverConfigSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{_t('Return to app')}
|
||||
let header;
|
||||
let errorText;
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
if (theme === 'status' && this.state.errorText) {
|
||||
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
} else {
|
||||
header = <h2>{ _t('Create an account') }</h2>;
|
||||
if (this.state.errorText) {
|
||||
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let signIn;
|
||||
if (!this.state.doingUIAuth) {
|
||||
signIn = (
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader
|
||||
icon={this.state.teamSelected ?
|
||||
|
@ -393,15 +464,14 @@ module.exports = React.createClass({
|
|||
this.state.teamSelected.domain + "/icon.png" :
|
||||
null}
|
||||
/>
|
||||
<h2>{_t('Create an account')}</h2>
|
||||
{registerBody}
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{_t('I already have an account')}
|
||||
</a>
|
||||
{returnToAppJsx}
|
||||
{ header }
|
||||
{ registerBody }
|
||||
{ signIn }
|
||||
{ errorText }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,10 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
@ -25,16 +26,20 @@ module.exports = React.createClass({
|
|||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: React.PropTypes.string, // ID for generating hash colours
|
||||
title: React.PropTypes.string, // onHover title text
|
||||
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -42,7 +47,7 @@ module.exports = React.createClass({
|
|||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -50,17 +55,26 @@ module.exports = React.createClass({
|
|||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
var newState = this._getState(nextProps);
|
||||
var newImageUrls = newState.imageUrls;
|
||||
var oldImageUrls = this.state.imageUrls;
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (var i = 0; i < newImageUrls.length; i++) {
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
|
@ -69,35 +83,52 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
var urls = props.urls || [];
|
||||
const urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
|
||||
var defaultImageUrl = null;
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
var nextIndex = this.state.urlsIndex + 1;
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -111,32 +142,32 @@ module.exports = React.createClass({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
var idx = 0;
|
||||
var initial = name[0];
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
var chars = 1;
|
||||
var first = name.charCodeAt(idx);
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
var second = name.charCodeAt(idx+1);
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
var firstChar = name.substring(idx, idx+chars);
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
|
@ -152,7 +183,7 @@ module.exports = React.createClass({
|
|||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{initialLetter}
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
);
|
||||
const imgNode = (
|
||||
|
@ -165,15 +196,15 @@ module.exports = React.createClass({
|
|||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -198,5 +229,5 @@ module.exports = React.createClass({
|
|||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,10 +24,12 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -52,11 +54,11 @@ export default React.createClass({
|
|||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
|
||||
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar
|
||||
name={this.props.groupId[1]}
|
||||
name={groupName || this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
|
|
|
@ -16,24 +16,25 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
member: PropTypes.object.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: React.PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
viewUserOnClick: React.PropTypes.bool,
|
||||
title: React.PropTypes.string,
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -63,14 +64,14 @@ module.exports = React.createClass({
|
|||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod)
|
||||
props.resizeMethod),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
|
@ -83,7 +84,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick}/>
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,11 +13,13 @@ 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 ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from "../../../index";
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -26,11 +28,12 @@ module.exports = React.createClass({
|
|||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
viewAvatarOnClick: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -44,13 +47,35 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props)
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
) return;
|
||||
|
||||
this.setState({
|
||||
urls: this.getImageUrls(this.props),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -61,11 +86,10 @@ module.exports = React.createClass({
|
|||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -79,76 +103,62 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
var mlist = props.room.currentState.members;
|
||||
var userIds = [];
|
||||
// for .. in optimisation to return early if there are >2 keys
|
||||
for (var uid in mlist) {
|
||||
if (mlist.hasOwnProperty(uid)) {
|
||||
userIds.push(uid);
|
||||
}
|
||||
if (userIds.length > 2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = mlist[userIds[1]];
|
||||
} else {
|
||||
theOtherGuy = mlist[userIds[0]];
|
||||
}
|
||||
return theOtherGuy.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getFallbackAvatar: function(props) {
|
||||
let roomId = null;
|
||||
if (props.oobData && props.oobData.roomId) {
|
||||
roomId = this.props.oobData.roomId;
|
||||
} else if (props.room) {
|
||||
roomId = props.room.roomId;
|
||||
} else {
|
||||
const room = props.room;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
return Avatar.defaultAvatarUrlForString(roomId);
|
||||
onRoomAvatarClick: function() {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
null, null, null, false);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: avatarUrl,
|
||||
name: this.props.room.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {room, oobData, ...otherProps} = this.props;
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||
|
||||
var roomName = room ? room.name : oobData.name;
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls} />
|
||||
urls={this.state.urls}
|
||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/*
|
||||
* This component can be used to display generic HTML content in a contextual
|
||||
* menu.
|
||||
*/
|
||||
|
||||
|
||||
export default class GenericElementContextMenu extends React.Component {
|
||||
static PropTypes = {
|
||||
element: PropTypes.element.isRequired,
|
||||
// Function to be called when the parent window is resized
|
||||
// This can be used to reposition or close the menu on resize and
|
||||
// ensure that it is not displayed in a stale position.
|
||||
onResize: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.resize = this.resize.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.resize = this.resize.bind(this);
|
||||
window.addEventListener("resize", this.resize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
}
|
||||
|
||||
resize() {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>{ this.props.element }</div>;
|
||||
}
|
||||
}
|
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>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.instanceOf(Group).isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onClickReject() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
try {
|
||||
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||
} catch (e) {
|
||||
console.error("Error rejecting community invite: ", e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to reject invite"),
|
||||
});
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
353
src/components/views/context_menus/MessageContextMenu.js
Normal file
353
src/components/views/context_menus/MessageContextMenu.js
Normal file
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import Resend from '../../../Resend';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {makeEventPermalink} from '../../../matrix-to';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageContextMenu',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||
eventTileOps: PropTypes.object,
|
||||
|
||||
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||
collapseReplyThread: PropTypes.func,
|
||||
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: 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);
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
content: this.props.mxEvent.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewClearSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
|
||||
// FIXME: _clearEvent is private
|
||||
content: this.props.mxEvent._clearEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onRedactClick: function() {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.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}),
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onCancelSendClick: function() {
|
||||
Resend.removeFromQueue(this.props.mxEvent);
|
||||
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);
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
},
|
||||
|
||||
onUnhidePreviewClick: function() {
|
||||
if (this.props.eventTileOps) {
|
||||
this.props.eventTileOps.unhideWidget();
|
||||
}
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onQuoteClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onPermalinkClick: function(e: Event) {
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReplyClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onCollapseReplyThreadClick: function() {
|
||||
this.props.collapseReplyThread();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const eventStatus = this.props.mxEvent.status;
|
||||
let resendButton;
|
||||
let redactButton;
|
||||
let cancelButton;
|
||||
let forwardButton;
|
||||
let pinButton;
|
||||
let viewClearSourceButton;
|
||||
let unhidePreviewButton;
|
||||
let externalURLButton;
|
||||
let quoteButton;
|
||||
let replyButton;
|
||||
let collapseReplyThread;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && 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>
|
||||
);
|
||||
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
{ _t('View Decrypted Source') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||
</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" &&
|
||||
isUrlPermitted(this.props.mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={this.props.mxEvent.event.content.external_url}
|
||||
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ resendButton }
|
||||
{ redactButton }
|
||||
{ cancelButton }
|
||||
{ forwardButton }
|
||||
{ pinButton }
|
||||
{ viewSourceButton }
|
||||
{ viewClearSourceButton }
|
||||
{ unhidePreviewButton }
|
||||
{ permalinkButton }
|
||||
{ quoteButton }
|
||||
{ replyButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
366
src/components/views/context_menus/RoomTileContextMenu.js
Normal file
366
src/components/views/context_menus/RoomTileContextMenu.js
Normal file
|
@ -0,0 +1,366 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import * as Rooms from '../../../Rooms';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import Modal from '../../../Modal';
|
||||
import RoomListActions from '../../../actions/RoomListActions';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTileContextMenu',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: 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) {
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
Promise.delay(500).then(() => {
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
MatrixClientPeg.get(),
|
||||
this.props.room,
|
||||
tagNameOff, tagNameOn,
|
||||
undefined, 0,
|
||||
), true);
|
||||
|
||||
this.props.onFinished();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_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() {
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const newIsDirectMessage = !this.state.isDirectMessage;
|
||||
this.setState({
|
||||
isDirectMessage: newIsDirectMessage,
|
||||
});
|
||||
|
||||
Rooms.guessAndSetDMRoom(
|
||||
this.props.room, newIsDirectMessage,
|
||||
).delay(500).finally(() => {
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}, (err) => {
|
||||
const 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) {
|
||||
const errCode = err.errcode || _td("unknown error code");
|
||||
const 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) {
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const oldState = this.state.roomNotifState;
|
||||
const roomId = this.props.room.roomId;
|
||||
|
||||
this.setState({
|
||||
roomNotifState: newState,
|
||||
});
|
||||
RoomNotifs.setRoomNotifsState(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() {
|
||||
const alertMeClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
|
||||
});
|
||||
|
||||
const allNotifsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
|
||||
});
|
||||
|
||||
const mentionsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
|
||||
});
|
||||
|
||||
const 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 (noisy)') }
|
||||
</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 myMembership = this.props.room.getMyMembership();
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMembership !== 'join') {
|
||||
return this._renderLeaveMenu(myMembership);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
75
src/components/views/context_menus/TagTileContextMenu.js
Normal file
75
src/components/views/context_menus/TagTileContextMenu.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
||||
}
|
||||
|
||||
_onViewCommunityClick() {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.tag,
|
||||
});
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
_onRemoveClick() {
|
||||
dis.dispatch(TagOrderActions.removeTag(
|
||||
// XXX: Context menus don't have a MatrixClient context
|
||||
MatrixClientPeg.get(),
|
||||
this.props.tag,
|
||||
));
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return <div>
|
||||
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
|
||||
<TintableSvg
|
||||
className="mx_TagTileContextMenu_item_icon"
|
||||
src="img/icons-groups.svg"
|
||||
width="15"
|
||||
height="15"
|
||||
/>
|
||||
{ _t('View Community') }
|
||||
</div>
|
||||
<hr className="mx_TagTileContextMenu_separator" />
|
||||
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
|
||||
<img className="mx_TagTileContextMenu_item_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ _t('Remove') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomButton',
|
||||
propTypes: {
|
||||
onCreateRoom: React.PropTypes.func,
|
||||
onCreateRoom: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -36,7 +37,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,10 +16,11 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var Presets = {
|
||||
const Presets = {
|
||||
PrivateChat: "private_chat",
|
||||
PublicChat: "public_chat",
|
||||
Custom: "custom",
|
||||
|
@ -28,8 +29,8 @@ var Presets = {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
preset: React.PropTypes.string
|
||||
onChange: PropTypes.func,
|
||||
preset: PropTypes.string,
|
||||
},
|
||||
|
||||
Presets: Presets,
|
||||
|
@ -47,10 +48,10 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
|
||||
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
|
||||
<option value={this.Presets.Custom}>{_t("Custom")}</option>
|
||||
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
|
||||
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
|
||||
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -22,9 +23,9 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// Specifying a homeserver will make magical things happen when you,
|
||||
// e.g. start typing in the room alias box.
|
||||
homeserver: React.PropTypes.string,
|
||||
alias: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
homeserver: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -35,10 +36,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getAliasLocalpart: function() {
|
||||
var room_alias = this.props.alias;
|
||||
let room_alias = this.props.alias;
|
||||
|
||||
if (room_alias && this.props.homeserver) {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
||||
room_alias = room_alias.slice(1, -suffix.length);
|
||||
}
|
||||
|
@ -52,22 +53,22 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
var target = ev.target;
|
||||
var curr_val = ev.target.value;
|
||||
const target = ev.target;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "") {
|
||||
var self = this;
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
target.value = "#:" + self.props.homeserver;
|
||||
target.setSelectionRange(1, 1);
|
||||
}, 0);
|
||||
} else {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
setTimeout(function() {
|
||||
target.setSelectionRange(
|
||||
curr_val.startsWith("#") ? 1 : 0,
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
@ -75,7 +76,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var curr_val = ev.target.value;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "#:" + this.props.homeserver) {
|
||||
|
@ -84,8 +85,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (curr_val != "") {
|
||||
var new_val = ev.target.value;
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
let new_val = ev.target.value;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
||||
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
||||
ev.target.value = new_val;
|
||||
|
@ -97,7 +98,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias}/>
|
||||
value={this.props.alias} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,22 +17,31 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "UserPickerDialog",
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
propTypes: {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.node,
|
||||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode: PropTypes.node,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
|
@ -40,6 +49,12 @@ module.exports = React.createClass({
|
|||
focus: PropTypes.bool,
|
||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
groupId: PropTypes.string,
|
||||
// The type of entity to search for. Default: 'user'.
|
||||
pickerType: PropTypes.oneOf(['user', 'room']),
|
||||
// Whether the current user should be included in the addresses returned. Only
|
||||
// applicable when pickerType is `user`. Default: false.
|
||||
includeSelf: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -47,6 +62,8 @@ module.exports = React.createClass({
|
|||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -56,7 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
userList: [],
|
||||
selectedList: [],
|
||||
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
|
@ -66,10 +83,9 @@ module.exports = React.createClass({
|
|||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of UserAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -81,14 +97,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
let userList = this.state.userList.slice();
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local userList
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
userList = this._addInputToList();
|
||||
if (userList === null) return;
|
||||
selectedList = this._addInputToList();
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, userList);
|
||||
this.props.onFinished(true, selectedList);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -108,18 +124,18 @@ module.exports = React.createClass({
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.userList.length - 1)();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value == '') {
|
||||
if (this.refs.textinput.value === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
|
@ -138,17 +154,29 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupSearch(query);
|
||||
} else if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
} else if (this.props.pickerType === 'room') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupRoomSearch(query);
|
||||
} else {
|
||||
this._doRoomSearch(query);
|
||||
}
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
console.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: [],
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
|
@ -157,11 +185,11 @@ module.exports = React.createClass({
|
|||
|
||||
onDismissed: function(index) {
|
||||
return () => {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.splice(index, 1);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -175,16 +203,129 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSelected: function(index) {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(this.state.queryList[index]);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_doNaiveGroupSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||
const results = [];
|
||||
resp.chunk.forEach((u) => {
|
||||
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(userIdMatch || displayNameMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
user_id: u.user_id,
|
||||
avatar_url: u.avatar_url,
|
||||
display_name: u.displayname,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst searching group rooms: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_doNaiveGroupRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const results = [];
|
||||
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
room_id: r.room_id,
|
||||
avatar_url: r.avatar_url,
|
||||
name: r.name || r.canonical_alias,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const results = [];
|
||||
rooms.forEach((room) => {
|
||||
let rank = Infinity;
|
||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
let aliasMatch = false;
|
||||
let shortestMatchingAliasLength = Infinity;
|
||||
aliases.forEach((alias) => {
|
||||
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||
aliasMatch = true;
|
||||
if (shortestMatchingAliasLength > alias.length) {
|
||||
shortestMatchingAliasLength = alias.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(nameMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aliasMatch) {
|
||||
// A shorter matching alias will give a better rank
|
||||
rank = shortestMatchingAliasLength;
|
||||
}
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
rank,
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by rank ascending (a high rank being less relevant)
|
||||
const sortedResults = results.sort((a, b) => {
|
||||
return a.rank - b.rank;
|
||||
});
|
||||
|
||||
this._processResults(sortedResults, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doUserDirectorySearch: function(query) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -244,18 +385,31 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
@ -265,18 +419,18 @@ module.exports = React.createClass({
|
|||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
queryList.unshift({
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
if (addrType === 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
suggestedList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
|
@ -291,27 +445,34 @@ module.exports = React.createClass({
|
|||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.avatarMxc = room.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(addrObj);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(addrObj);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return userList;
|
||||
return selectedList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
|
@ -337,7 +498,7 @@ module.exports = React.createClass({
|
|||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
|
@ -350,24 +511,42 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.userList.length > 0) {
|
||||
if (this.state.selectedList.length > 0) {
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea key={this.state.userList.length}
|
||||
<textarea key={this.state.selectedList.length}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
|
@ -382,36 +561,32 @@ module.exports = React.createClass({
|
|||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished} title={this.props.title}>
|
||||
<div className="mx_ChatInviteDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>
|
||||
|
@ -419,13 +594,12 @@ module.exports = React.createClass({
|
|||
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,10 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -31,55 +38,103 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// onFinished callback to call when Escape is pressed
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// Take a boolean which is true if the dialog was dismissed
|
||||
// with a positive / confirm action or false if it was
|
||||
// cancelled (BaseDialog itself only calls this with false).
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// callback to call when Enter is pressed
|
||||
onEnterPressed: React.PropTypes.func,
|
||||
// Whether the dialog should have a 'close' button that will
|
||||
// cause the dialog to be cancelled. This should only be set
|
||||
// to false if there is nothing the app can sensibly do if the
|
||||
// dialog is cancelled, eg. "We can't restore your session and
|
||||
// the app cannot work". Default: true.
|
||||
hasCancel: PropTypes.bool,
|
||||
|
||||
// called when a key is pressed
|
||||
onKeyDown: PropTypes.func,
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// Title for the dialog.
|
||||
// (could probably actually be something more complicated than a string if desired)
|
||||
title: React.PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: React.PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element
|
||||
titleClass: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
hasCancel: true,
|
||||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (e.keyCode === KeyCode.ESCAPE) {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished();
|
||||
} else if (e.keyCode === KeyCode.ENTER) {
|
||||
if (this.props.onEnterPressed) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onEnterPressed(e);
|
||||
}
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
},
|
||||
|
||||
_onCancelClick: function(e) {
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={this.props.className}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
{ cancelButton }
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
203
src/components/views/dialogs/BugReportDialog.js
Normal file
203
src/components/views/dialogs/BugReportDialog.js
Normal file
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
Copyright 2017 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class BugReportDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
sendLogs: true,
|
||||
busy: false,
|
||||
err: null,
|
||||
issueUrl: "",
|
||||
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._onIssueUrlChange = this._onIssueUrlChange.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 userText =
|
||||
(this.state.text.length > 0 ? this.state.text + '\n\n': '') + 'Issue: ' +
|
||||
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
||||
|
||||
this.setState({ busy: true, progress: null, err: null });
|
||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
||||
|
||||
require(['../../../rageshake/submit-rageshake'], (s) => {
|
||||
s(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
|
||||
title: _t('Logs sent'),
|
||||
description: _t('Thank you!'),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
err: _t("Failed to send logs: ") + `${err.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onTextChange(ev) {
|
||||
this.setState({ text: ev.target.value });
|
||||
}
|
||||
|
||||
_onIssueUrlChange(ev) {
|
||||
this.setState({ issueUrl: 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");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
{this.state.progress} ...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>
|
||||
{ _t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
"username, the IDs or aliases of the rooms or groups you " +
|
||||
"have visited and the usernames of other users. They do " +
|
||||
"not contain messages.",
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
|
||||
{},
|
||||
{
|
||||
a: (sub) => <a
|
||||
target="_blank"
|
||||
href="https://github.com/vector-im/riot-web/issues/new"
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<div className="mx_BugReportDialog_field_container">
|
||||
<label
|
||||
htmlFor="mx_BugReportDialog_issueUrl"
|
||||
className="mx_BugReportDialog_field_label"
|
||||
>
|
||||
{ _t("GitHub issue link:") }
|
||||
</label>
|
||||
<input
|
||||
id="mx_BugReportDialog_issueUrl"
|
||||
type="text"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
onChange={this._onIssueUrlChange}
|
||||
value={this.state.issueUrl}
|
||||
placeholder="https://github.com/vector-im/riot-web/issues/1337"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_BugReportDialog_field_container">
|
||||
<label
|
||||
htmlFor="mx_BugReportDialog_notes_label"
|
||||
className="mx_BugReportDialog_field_label"
|
||||
>
|
||||
{ _t("Notes:") }
|
||||
</label>
|
||||
<textarea
|
||||
className="mx_BugReportDialog_field_input"
|
||||
rows={5}
|
||||
onChange={this._onTextChange}
|
||||
value={this.state.text}
|
||||
/>
|
||||
</div>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Send logs")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
94
src/components/views/dialogs/ChangelogDialog.js
Normal file
94
src/components/views/dialogs/ChangelogDialog.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const REPOS = ['vector-im/riot-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
||||
|
||||
export default class ChangelogDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const version = this.props.newVersion.split('-');
|
||||
const version2 = this.props.version.split('-');
|
||||
if(version == null || version2 == null) return;
|
||||
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
|
||||
for(let i=0; i<REPOS.length; i++) {
|
||||
const oldVersion = version2[2*i];
|
||||
const newVersion = version[2*i];
|
||||
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
|
||||
if(body == null) return;
|
||||
this.setState({[REPOS[i]]: JSON.parse(body).commits});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_elementsForCommit(commit) {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
||||
{commit.commit.message}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
||||
const logs = REPOS.map(repo => {
|
||||
if (this.state[repo] == null) return <Spinner key={repo} />;
|
||||
return (
|
||||
<div key={repo}>
|
||||
<h2>{repo}</h2>
|
||||
<ul>
|
||||
{this.state[repo].map(this._elementsForCommit)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const content = (
|
||||
<div className="mx_ChangelogDialog_content">
|
||||
{this.props.version == null || this.props.newVersion == null ? <h2>{_t("Unavailable")}</h2> : logs}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Changelog")}
|
||||
description={content}
|
||||
button={_t("Update")}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChangelogDialog.propTypes = {
|
||||
version: React.PropTypes.string.isRequired,
|
||||
newVersion: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -27,6 +29,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -51,18 +54,16 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
const isInvite = room.getMyMembership() === "invite";
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
isInvite={isInvite}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -108,6 +109,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
|
@ -127,7 +132,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
||||
</AccessibleButton>;
|
||||
content = <div className="mx_Dialog_content">
|
||||
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ _t('You already have existing direct chats with this user:') }
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{ this.state.tiles }
|
||||
|
@ -137,6 +142,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
} else {
|
||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
title = _t('Start chatting');
|
||||
|
||||
|
@ -144,7 +150,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
if (this.state.busyProfile) {
|
||||
profile = <Spinner />;
|
||||
} else if (this.state.profileError) {
|
||||
profile = <div className="error">
|
||||
profile = <div className="error" role="alert">
|
||||
Unable to load profile information for { this.props.userId }
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -155,30 +161,28 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
width={48} height={48}
|
||||
/>
|
||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||
{this.state.profile.displayName || this.props.userId}
|
||||
{ this.state.profile.displayName || this.props.userId }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
content = <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>
|
||||
{ _t('Click on the button below to start chatting!') }
|
||||
</p>
|
||||
{ profile }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
|
||||
{ _t('Start Chatting') }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={ this.props.onFinished.bind(false) }
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
|
@ -186,10 +190,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
ChatCreateOrReuseDialog.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onNewDMClick: React.PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
onNewDMClick: PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
|
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
danger: false,
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = _t("Confirm Removal");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': false,
|
||||
});
|
||||
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
{_t("Remove")}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,9 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -30,15 +32,21 @@ import classnames from 'classnames';
|
|||
export default React.createClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason: React.PropTypes.bool,
|
||||
danger: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
askReason: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
|
@ -68,13 +76,11 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
});
|
||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||
|
||||
let reasonBox;
|
||||
if (this.props.askReason) {
|
||||
|
@ -83,7 +89,7 @@ export default React.createClass({
|
|||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
placeholder={ _t("Reason") }
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</form>
|
||||
|
@ -91,30 +97,39 @@ export default React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let avatar;
|
||||
let name;
|
||||
let userId;
|
||||
if (this.props.member) {
|
||||
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
||||
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
{ avatar }
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
</div>
|
||||
{reasonBox}
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{this.props.action}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{ reasonBox }
|
||||
<DialogButtons primaryButton={this.props.action}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={confirmButtonClass}
|
||||
focus={!this.props.askReason}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -21,10 +21,6 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// We match fairly liberally and leave it up to the server to reject if
|
||||
// there are invalid characters etc.
|
||||
const GROUP_REGEX = /^\+(.*?):(.*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
|
@ -58,25 +54,16 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
let error = null;
|
||||
if (parsedGroupId === null) {
|
||||
error = _t(
|
||||
"Group IDs must be of the form +localpart:%(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
} else {
|
||||
const domain = parsedGroupId[1];
|
||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||
error = _t(
|
||||
"It is currently only possible to create groups on your own home server: "+
|
||||
"use a group ID ending with %(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
}
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
// Reset createError to get rid of now stale error message
|
||||
createError: null,
|
||||
});
|
||||
return error;
|
||||
},
|
||||
|
@ -86,19 +73,19 @@ export default React.createClass({
|
|||
|
||||
if (this._checkGroupId()) return;
|
||||
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
const profile = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({creating: true});
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: parsedGroupId[0],
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
|
@ -112,22 +99,6 @@ export default React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a string that may be a group ID
|
||||
* If the string is a valid group ID, return a list of [localpart, domain],
|
||||
* otherwise return null.
|
||||
*
|
||||
* @param {string} groupId The ID of the group
|
||||
* @return {string[]} array of localpart, domain
|
||||
*/
|
||||
_parseGroupId: function(groupId) {
|
||||
const matches = GROUP_REGEX.exec(this.state.groupId);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
@ -141,22 +112,21 @@ export default React.createClass({
|
|||
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
||||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{_t('Room creation failed')}</div>
|
||||
<div>{this.state.createError.message}</div>
|
||||
createErrorNode = <div className="error" role="alert">
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Group')}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
|
@ -169,28 +139,33 @@ export default React.createClass({
|
|||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
size="64"
|
||||
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{this.state.groupIdError}
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{createErrorNode}
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this._onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
79
src/components/views/dialogs/CreateRoomDialog.js
Normal file
79
src/components/views/dialogs/CreateRoomDialog.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Create Room')}
|
||||
>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={_t('Create Room')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
@ -32,10 +33,21 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
const deactivationPreferences =
|
||||
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
||||
|
||||
const shouldErase = (
|
||||
deactivationPreferences &&
|
||||
deactivationPreferences.getContent() &&
|
||||
deactivationPreferences.getContent().shouldErase
|
||||
) || false;
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
busy: false,
|
||||
shouldErase,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
@ -46,19 +58,55 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onOk() {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
_onEraseFieldChange(ev) {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().deactivateAccount({
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
}).done(() => {
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}, (err) => {
|
||||
|
||||
// Before we deactivate the account insert an event into
|
||||
// the user's account data indicating that they wish to be
|
||||
// erased from the homeserver.
|
||||
//
|
||||
// We do this because the API for erasing after deactivation
|
||||
// might not be supported by the connected homeserver. Leaving
|
||||
// an indication in account data is only best-effort, and
|
||||
// in the worse case, the HS maintainer would have to run a
|
||||
// script to erase deactivated accounts that have shouldErase
|
||||
// set to true in im.riot.account_deactivation_preferences.
|
||||
//
|
||||
// Note: The preferences are scoped to Riot, hence the
|
||||
// "im.riot..." event type.
|
||||
//
|
||||
// Note: This may have already been set on previous attempts
|
||||
// where, for example, the user entered the wrong password.
|
||||
// This is fine because the UI always indicates the preference
|
||||
// prior to us calling `deactivateAccount`.
|
||||
try {
|
||||
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
||||
shouldErase: this.state.shouldErase,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: _t('Failed to indicate account erasure'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
const auth = {
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
};
|
||||
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||
} catch (err) {
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
||||
|
@ -69,7 +117,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
busy: false,
|
||||
errStr: errStr,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
|
@ -77,13 +130,14 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let passwordBoxClass = '';
|
||||
|
||||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
@ -94,30 +148,74 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
{_t("Deactivate Account")}
|
||||
</div>
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
|
||||
<p>{ _t(
|
||||
"This will make your account permanently unusable. " +
|
||||
"You will not be able to log in, and no one will be able to re-register the same " +
|
||||
"user ID. " +
|
||||
"This will cause your account to leave all rooms it is participating in, and it " +
|
||||
"will remove your account details from your identity server. " +
|
||||
"<b>This action is irreversible.</b>",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{_t("This action is irreversible.")}</p>
|
||||
<p>{ _t(
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you " +
|
||||
"have sent.</b> " +
|
||||
"If you would like us to forget your messages, please tick the box below.",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
<p>{ _t(
|
||||
"Message visibility in Matrix is similar to email. " +
|
||||
"Our forgetting your messages means that messages you have sent will not be shared " +
|
||||
"with any new or unregistered users, but registered users who already have access " +
|
||||
"to these messages will still have access to their copy.",
|
||||
) }</p>
|
||||
|
||||
<p>{_t("Password")}:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{error}
|
||||
<div className="mx_DeactivateAccountDialog_input_section">
|
||||
<p>
|
||||
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||
<input
|
||||
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||
type="checkbox"
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
/>
|
||||
{ _t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
) }
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
|
@ -125,16 +223,16 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
onClick={this._onOk}
|
||||
disabled={!okEnabled}
|
||||
>
|
||||
{okLabel}
|
||||
{ okLabel }
|
||||
</button>
|
||||
|
||||
{cancelButton}
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeactivateAccountDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
@ -28,25 +29,25 @@ export default function DeviceVerifyDialog(props) {
|
|||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("To verify that this device can be trusted, please contact its " +
|
||||
{ _t("To verify that this device can be trusted, please contact its " +
|
||||
"owner using some other means (e.g. in person or a phone call) " +
|
||||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:")}
|
||||
"for this device matches the key below:") }
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{_t("If it matches, press the verify button below. " +
|
||||
{ _t("If it matches, press the verify button below. " +
|
||||
"If it doesn't, then someone else is intercepting this device " +
|
||||
"and you probably want to press the blacklist button instead.")}
|
||||
"and you probably want to press the blacklist button instead.") }
|
||||
</p>
|
||||
<p>
|
||||
{_t("In future this verification process will be more sophisticated.")}
|
||||
{ _t("In future this verification process will be more sophisticated.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
|
|||
}
|
||||
|
||||
DeviceVerifyDialog.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
663
src/components/views/dialogs/DevtoolsDialog.js
Normal file
663
src/components/views/dialogs/DevtoolsDialog.js
Normal file
|
@ -0,0 +1,663 @@
|
|||
/*
|
||||
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 '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../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" autoFocus={true} />
|
||||
</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_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</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_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</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>;
|
||||
}
|
||||
}
|
||||
|
||||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static filterChildren(children, query) {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
};
|
||||
|
||||
getChildren = (start: number, end: number) => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
autoFocus={true}
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</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.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
this.onQueryStateKey = this.onQueryStateKey.bind(this);
|
||||
|
||||
this.state = {
|
||||
eventType: null,
|
||||
event: null,
|
||||
editing: false,
|
||||
|
||||
queryEventType: '',
|
||||
queryStateKey: '',
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
onQueryEventType(filterEventType) {
|
||||
this.setState({ queryEventType: filterEventType });
|
||||
}
|
||||
|
||||
onQueryStateKey(filterStateKey) {
|
||||
this.setState({ queryStateKey: filterStateKey });
|
||||
}
|
||||
|
||||
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">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
<button onClick={this.editEv}>{ _t('Edit') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let list = null;
|
||||
|
||||
const classes = 'mx_DevTools_RoomStateExplorer_button';
|
||||
if (this.state.eventType === null) {
|
||||
list = <FilteredList query={this.state.queryEventType} onChange={this.onQueryEventType}>
|
||||
{
|
||||
Object.keys(this.roomStateEvents).map((evType) => {
|
||||
const stateGroup = this.roomStateEvents[evType];
|
||||
const stateKeys = Object.keys(stateGroup);
|
||||
|
||||
let onClickFn;
|
||||
if (stateKeys.length === 1 && stateKeys[0] === '') {
|
||||
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
|
||||
} else {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
}
|
||||
|
||||
return <button className={classes} key={evType} onClick={onClickFn}>
|
||||
{ evType }
|
||||
</button>;
|
||||
})
|
||||
}
|
||||
</FilteredList>;
|
||||
} else {
|
||||
const stateGroup = this.roomStateEvents[this.state.eventType];
|
||||
|
||||
list = <FilteredList query={this.state.queryStateKey} onChange={this.onQueryStateKey}>
|
||||
{
|
||||
Object.keys(stateGroup).map((stateKey) => {
|
||||
const ev = stateGroup[stateKey];
|
||||
return <button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
|
||||
{ stateKey }
|
||||
</button>;
|
||||
})
|
||||
}
|
||||
</FilteredList>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
{ list }
|
||||
</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.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
|
||||
this.state = {
|
||||
isRoomAccountData: false,
|
||||
event: null,
|
||||
editing: false,
|
||||
|
||||
queryEventType: '',
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
onQueryEventType(queryEventType) {
|
||||
this.setState({ queryEventType });
|
||||
}
|
||||
|
||||
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_DevTools_content">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</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 query={this.state.queryEventType} onChange={this.onQueryEventType}>
|
||||
{ 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,20 +26,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -51,23 +52,19 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description || _t('An error has occurred.')}
|
||||
title={this.props.title || _t('Error')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
{this.props.button || _t('OK')}
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,28 +28,28 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
title: React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_onAuthFinished: function(success, result) {
|
||||
|
@ -72,19 +73,20 @@ export default React.createClass({
|
|||
let content;
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
<div>
|
||||
<div>{this.state.authError.message || this.state.authError.toString()}</div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
autoFocus="true"
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
{ _t("Dismiss") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
|
@ -99,8 +101,9 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
|||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
|
@ -30,10 +31,10 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
deviceId: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -54,7 +55,7 @@ export default React.createClass({
|
|||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if(!deviceInfo) {
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
|
@ -116,27 +117,27 @@ export default React.createClass({
|
|||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = "You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.";
|
||||
text = _td("You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = "Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.";
|
||||
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{text}</p>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
{_t('Start verification')}
|
||||
<button onClick={this._onVerifyClicked} autoFocus="true">
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{_t('Share without verifying')}
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{_t('Ignore request')}
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -153,8 +154,8 @@ export default React.createClass({
|
|||
content = this._renderContent();
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t('Loading device info...')}</p>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ _t('Loading device info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
@ -164,8 +165,9 @@ export default React.createClass({
|
|||
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,18 +16,20 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
danger: PropTypes.bool,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -36,6 +39,7 @@ export default React.createClass({
|
|||
extraButtons: null,
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -49,26 +53,29 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
) : null;
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let primaryButtonClass = "";
|
||||
if (this.props.danger) {
|
||||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button || _t('OK')}
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
{ this.props.extraButtons }
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoomUpgradeDialog',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._targetVersion = this.props.room.shouldUpgradeToVersion();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_onUpgradeClick: function() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
||||
title: _t("Failed to upgrade room"),
|
||||
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({busy: false});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let buttons;
|
||||
if (this.state.busy) {
|
||||
buttons = <Spinner />;
|
||||
} else {
|
||||
buttons = <DialogButtons
|
||||
primaryButton={_t(
|
||||
'Upgrade this room to version %(version)s',
|
||||
{version: this._targetVersion},
|
||||
)}
|
||||
primaryButtonClass="danger"
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={this._onUpgradeClick}
|
||||
focus={this.props.focus}
|
||||
onCancel={this._onCancelClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_RoomUpgradeDialog"
|
||||
onFinished={this.onCancelled}
|
||||
title={_t("Upgrade Room Version")}
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
hasCancel={true}
|
||||
>
|
||||
<p>
|
||||
{_t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room it its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
</p>
|
||||
<ol>
|
||||
<li>{_t("Create a new room with the same name, description and avatar")}</li>
|
||||
<li>{_t("Update any local room aliases to point to the new room")}</li>
|
||||
<li>{_t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room")}</li>
|
||||
<li>{_t("Put a link back to the old room at the start of the new room so people can see old messages")}</li>
|
||||
</ol>
|
||||
{buttons}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,18 +16,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'SessionRestoreErrorDialog',
|
||||
|
||||
propTypes: {
|
||||
error: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_sendBugReport: function() {
|
||||
|
@ -34,44 +36,74 @@ export default React.createClass({
|
|||
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||
},
|
||||
|
||||
_continueClicked: function() {
|
||||
this.props.onFinished(true);
|
||||
_onClearStorageClick: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
|
||||
title: _t("Sign out"),
|
||||
description:
|
||||
<div>{ _t("Log out and remove encryption keys?") }</div>,
|
||||
button: _t("Sign out"),
|
||||
danger: true,
|
||||
onFinished: this.props.onFinished,
|
||||
});
|
||||
},
|
||||
|
||||
_onRefreshClick: function() {
|
||||
// Is this likely to help? Probably not, but giving only one button
|
||||
// that clears your storage seems awful.
|
||||
window.location.reload(true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
let bugreport;
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const clearStorageButton = (
|
||||
<button onClick={this._onClearStorageClick} className="danger">
|
||||
{ _t("Clear Storage and Sign Out") }
|
||||
</button>
|
||||
);
|
||||
|
||||
let dialogButtons;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>
|
||||
{_tJsx(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
|
||||
onPrimaryButtonClick={this._sendBugReport}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
>
|
||||
{ clearStorageButton }
|
||||
</DialogButtons>;
|
||||
} else {
|
||||
dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
|
||||
onPrimaryButtonClick={this._onRefreshClick}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
>
|
||||
{ clearStorageButton }
|
||||
</DialogButtons>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Unable to restore session')}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
||||
"you continue, you will need to log in again, and encrypted chat " +
|
||||
"history will be unreadable.")}</p>
|
||||
title={_t('Unable to restore session')}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
|
||||
|
||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.")}</p>
|
||||
<p>{ _t(
|
||||
"If you have previously used a more recent version of Riot, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.",
|
||||
) }</p>
|
||||
|
||||
{bugreport}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{_t("Continue anyway")}
|
||||
</button>
|
||||
<p>{ _t(
|
||||
"Clearing your browser's storage may fix the problem, but will sign you " +
|
||||
"out and cause any encrypted chat history to become unreadable.",
|
||||
) }</p>
|
||||
</div>
|
||||
{ dialogButtons }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
|
@ -30,19 +32,16 @@ import Modal from '../../../Modal';
|
|||
export default React.createClass({
|
||||
displayName: 'SetEmailDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
emailAddress: null,
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
},
|
||||
|
||||
onEmailAddressChanged: function(value) {
|
||||
this.setState({
|
||||
emailAddress: value,
|
||||
|
@ -129,19 +128,22 @@ export default React.createClass({
|
|||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
initialValue={this.state.emailAddress}
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
placeholder={ _t("Email address") }
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onEmailAddressChanged } />;
|
||||
blurToCancel={false}
|
||||
onValueChanged={this.onEmailAddressChanged} />;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetEmailDialog"
|
||||
onFinished={this.onCancelled}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
<p id='mx_Dialog_content'>
|
||||
{ _t('This will allow you to reset your password and receive notifications.') }
|
||||
</p>
|
||||
{ emailInput }
|
||||
|
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
|
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
|||
export default React.createClass({
|
||||
displayName: 'SetMxIdDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
// Called when the user requests to register with a different homeserver
|
||||
onDifferentServerClicked: React.PropTypes.func.isRequired,
|
||||
onDifferentServerClicked: PropTypes.func.isRequired,
|
||||
// Called if the user wants to switch to login instead
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -226,7 +227,7 @@ export default React.createClass({
|
|||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
||||
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
|
@ -234,14 +235,14 @@ export default React.createClass({
|
|||
"error": Boolean(this.state.usernameError),
|
||||
"success": usernameAvailable,
|
||||
});
|
||||
usernameIndicator = <div className={usernameIndicatorClasses}>
|
||||
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
|
||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let authErrorIndicator = null;
|
||||
if (this.state.authError) {
|
||||
authErrorIndicator = <div className="error">
|
||||
authErrorIndicator = <div className="error" role="alert">
|
||||
{ this.state.authError }
|
||||
</div>;
|
||||
}
|
||||
|
@ -253,8 +254,9 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_SetMxIdDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('To get started, please pick a username!')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref="input_value" value={this.state.username}
|
||||
autoFocus={true}
|
||||
|
@ -267,25 +269,22 @@ export default React.createClass({
|
|||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
||||
],
|
||||
)}
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
|
|
134
src/components/views/dialogs/SetPasswordDialog.js
Normal file
134
src/components/views/dialogs/SetPasswordDialog.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../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.props.onFinished();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_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>
|
||||
);
|
||||
},
|
||||
});
|
224
src/components/views/dialogs/ShareDialog.js
Normal file
224
src/components/views/dialogs/ShareDialog.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
img: 'img/social/facebook.png',
|
||||
url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
}, {
|
||||
name: 'Twitter',
|
||||
img: 'img/social/twitter-2.png',
|
||||
url: (url) => `https://twitter.com/home?status=${url}`,
|
||||
}, /* // icon missing
|
||||
name: 'Google Plus',
|
||||
img: 'img/social/',
|
||||
url: (url) => `https://plus.google.com/share?url=${url}`,
|
||||
},*/ {
|
||||
name: 'LinkedIn',
|
||||
img: 'img/social/linkedin.png',
|
||||
url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
|
||||
}, {
|
||||
name: 'Reddit',
|
||||
img: 'img/social/reddit.png',
|
||||
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||
}, {
|
||||
name: 'email',
|
||||
img: 'img/social/email-1.png',
|
||||
url: (url) => `mailto:?body=${url}`,
|
||||
},
|
||||
];
|
||||
|
||||
export default class ShareDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
target: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Room),
|
||||
PropTypes.instanceOf(User),
|
||||
PropTypes.instanceOf(Group),
|
||||
PropTypes.instanceOf(RoomMember),
|
||||
PropTypes.instanceOf(MatrixEvent),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCopyClick = this.onCopyClick.bind(this);
|
||||
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
static onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
const {target} = e;
|
||||
ShareDialog._selectText(target);
|
||||
}
|
||||
|
||||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
this.setState({
|
||||
linkSpecificEvent: !this.state.linkSpecificEvent,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
||||
let checkbox;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
title = _t('Share Room');
|
||||
|
||||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
if (events.length > 0) {
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
}
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return <BaseDialog title={title}
|
||||
className='mx_ShareDialog'
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
|
||||
{ _t('COPY') }
|
||||
<div> </div>
|
||||
</a>
|
||||
</div>
|
||||
{ checkbox }
|
||||
<hr />
|
||||
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo="img/matrix-m.svg" />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
>
|
||||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -15,21 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'TextInputDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
value: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -58,27 +58,24 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> {this.props.description} </label>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,11 +16,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { markAllDevicesKnown } from '../../../cryptodevices';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -28,19 +32,19 @@ function DeviceListEntry(props) {
|
|||
|
||||
return (
|
||||
<li>
|
||||
<DeviceVerifyButtons device={ device } userId={ userId } />
|
||||
<DeviceVerifyButtons device={device} userId={userId} />
|
||||
{ device.deviceId }
|
||||
<br/>
|
||||
<br />
|
||||
{ device.getDisplayName() }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
DeviceListEntry.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// deviceinfo
|
||||
device: React.PropTypes.object.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -48,22 +52,22 @@ function UserUnknownDeviceList(props) {
|
|||
const {userId, userDevices} = props;
|
||||
|
||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||
<DeviceListEntry key={ deviceId } userId={ userId }
|
||||
device={ userDevices[deviceId] } />,
|
||||
<DeviceListEntry key={deviceId} userId={userId}
|
||||
device={userDevices[deviceId]} />,
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="mx_UnknownDeviceDialog_deviceList">
|
||||
{deviceListEntries}
|
||||
{ deviceListEntries }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
UserUnknownDeviceList.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// map from deviceid -> deviceinfo
|
||||
userDevices: React.PropTypes.object.isRequired,
|
||||
userDevices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -71,18 +75,18 @@ function UnknownDeviceList(props) {
|
|||
const {devices} = props;
|
||||
|
||||
const userListEntries = Object.keys(devices).map((userId) =>
|
||||
<li key={ userId }>
|
||||
<li key={userId}>
|
||||
<p>{ userId }:</p>
|
||||
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
|
||||
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
|
||||
</li>,
|
||||
);
|
||||
|
||||
return <ul>{userListEntries}</ul>;
|
||||
return <ul>{ userListEntries }</ul>;
|
||||
}
|
||||
|
||||
UnknownDeviceList.propTypes = {
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
devices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -90,90 +94,116 @@ export default React.createClass({
|
|||
displayName: 'UnknownDeviceDialog',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
|
||||
devices: PropTypes.object,
|
||||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Label for the button that marks all devices known and tries the send again
|
||||
sendAnywayLabel: PropTypes.string.isRequired,
|
||||
|
||||
// Label for the button that to send the event if you've verified all devices
|
||||
sendLabel: PropTypes.string.isRequired,
|
||||
|
||||
// function to retry the request once all devices are verified / known
|
||||
onSend: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Given we've now shown the user the unknown device, it is no longer
|
||||
// unknown to them. Therefore mark it as 'known'.
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Opening UnknownDeviceDialog');
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
|
||||
// XXX: Mutating props :/
|
||||
this.props.devices[userId][deviceId] = deviceInfo;
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
_onDismissClicked: function() {
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
_onSendAnywayClicked: function() {
|
||||
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
|
||||
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
_onSendClicked: function() {
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||
this.props.room.getBlacklistUnverifiedDevices();
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (blacklistUnverified) {
|
||||
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||
warning = (
|
||||
<h4>
|
||||
{_t("You are currently blacklisting unverified devices; to send " +
|
||||
"messages to these devices you must verify them.")}
|
||||
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||
"messages to these devices you must verify them.") }
|
||||
</h4>
|
||||
);
|
||||
} else {
|
||||
warning = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("We recommend you go through the verification process " +
|
||||
{ _t("We recommend you go through the verification process " +
|
||||
"for each device to confirm they belong to their legitimate owner, " +
|
||||
"but you can resend the message without verifying if you prefer.")}
|
||||
"but you can resend the message without verifying if you prefer.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let haveUnknownDevices = false;
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
const device = this.props.devices[userId][deviceId];
|
||||
if (device.isUnverified() && !device.isKnown()) {
|
||||
haveUnknownDevices = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
|
||||
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by escape");
|
||||
this.props.onFinished();
|
||||
}}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Room contains unknown devices')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<h4>
|
||||
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
|
||||
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||
</h4>
|
||||
{ warning }
|
||||
{_t("Unknown devices")}:
|
||||
{ _t("Unknown devices") }:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
{_t("Send anyway")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by OK");
|
||||
this.props.onFinished();
|
||||
}}>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
<DialogButtons primaryButton={sendButtonLabel}
|
||||
onPrimaryButtonClick={sendButtonOnClick}
|
||||
onCancel={this._onDismissClicked} />
|
||||
</BaseDialog>
|
||||
);
|
||||
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
||||
|
|
254
src/components/views/directory/NetworkDropdown.js
Normal file
254
src/components/views/directory/NetworkDropdown.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
||||
|
||||
const DEFAULT_ICON_URL = "img/network-matrix.svg";
|
||||
|
||||
export default class NetworkDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.dropdownRootElement = null;
|
||||
this.ignoreEvent = null;
|
||||
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.onRootClick = this.onRootClick.bind(this);
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
|
||||
this.onInputKeyUp = this.onInputKeyUp.bind(this);
|
||||
this.collectRoot = this.collectRoot.bind(this);
|
||||
this.collectInputTextBox = this.collectInputTextBox.bind(this);
|
||||
|
||||
this.inputTextBox = null;
|
||||
|
||||
const server = MatrixClientPeg.getHomeServerName();
|
||||
this.state = {
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstance: null,
|
||||
includeAllNetworks: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this.onDocumentClick, false);
|
||||
|
||||
// fire this now so the defaults can be set up
|
||||
this.props.onOptionChange(this.state.selectedServer, this.state.selectedInstance, this.state.includeAllNetworks);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onDocumentClick, false);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.expanded && this.inputTextBox) {
|
||||
this.inputTextBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onDocumentClick(ev) {
|
||||
// Close the dropdown if the user clicks anywhere that isn't
|
||||
// within our root element
|
||||
if (ev !== this.ignoreEvent) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRootClick(ev) {
|
||||
// This captures any clicks that happen within our elements,
|
||||
// such that we can then ignore them when they're seen by the
|
||||
// click listener on the document handler, ie. not close the
|
||||
// dropdown immediately after opening it.
|
||||
// NB. We can't just stopPropagation() because then the event
|
||||
// doesn't reach the React onClick().
|
||||
this.ignoreEvent = ev;
|
||||
}
|
||||
|
||||
onInputClick(ev) {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onMenuOptionClick(server, instance, includeAll) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstanceId: instance ? instance.instance_id : null,
|
||||
includeAll: includeAll,
|
||||
});
|
||||
this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
if (e.key == 'Enter') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: e.target.value,
|
||||
selectedNetwork: null,
|
||||
});
|
||||
this.props.onOptionChange(e.target.value, null);
|
||||
}
|
||||
}
|
||||
|
||||
collectRoot(e) {
|
||||
if (this.dropdownRootElement) {
|
||||
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
if (e) {
|
||||
e.addEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
this.dropdownRootElement = e;
|
||||
}
|
||||
|
||||
collectInputTextBox(e) {
|
||||
this.inputTextBox = e;
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
const options = [];
|
||||
|
||||
let servers = [];
|
||||
if (this.props.config.servers) {
|
||||
servers = servers.concat(this.props.config.servers);
|
||||
}
|
||||
|
||||
if (servers.indexOf(MatrixClientPeg.getHomeServerName()) == -1) {
|
||||
servers.unshift(MatrixClientPeg.getHomeServerName());
|
||||
}
|
||||
|
||||
// For our own HS, we can use the instance_ids given in the third party protocols
|
||||
// response to get the server to filter the room list by network for us.
|
||||
// We can't get thirdparty protocols for remote server yet though, so for those
|
||||
// we can only show the default room list.
|
||||
for (const server of servers) {
|
||||
options.push(this._makeMenuOption(server, null, true));
|
||||
if (server == MatrixClientPeg.getHomeServerName()) {
|
||||
options.push(this._makeMenuOption(server, null, false));
|
||||
if (this.props.protocols) {
|
||||
for (const proto of Object.keys(this.props.protocols)) {
|
||||
if (!this.props.protocols[proto].instances) continue;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
_makeMenuOption(server, instance, includeAll, handleClicks) {
|
||||
if (handleClicks === undefined) handleClicks = true;
|
||||
|
||||
let icon;
|
||||
let name;
|
||||
let span_class;
|
||||
let key;
|
||||
|
||||
if (!instance && includeAll) {
|
||||
key = server;
|
||||
name = server;
|
||||
span_class = 'mx_NetworkDropdown_menu_all';
|
||||
} else if (!instance) {
|
||||
key = server + '_all';
|
||||
name = 'Matrix';
|
||||
icon = <img src="img/network-matrix.svg" />;
|
||||
span_class = 'mx_NetworkDropdown_menu_network';
|
||||
} else {
|
||||
key = server + '_inst_' + instance.instance_id;
|
||||
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';
|
||||
}
|
||||
|
||||
const click_handler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
|
||||
|
||||
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
|
||||
{icon}
|
||||
<span className="mx_NetworkDropdown_menu_network">{name}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
render() {
|
||||
let current_value;
|
||||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
const menu_options = this._getMenuOptions();
|
||||
menu = <div className="mx_NetworkDropdown_menu">
|
||||
{menu_options}
|
||||
</div>;
|
||||
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
||||
/>
|
||||
} else {
|
||||
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
|
||||
current_value = this._makeMenuOption(
|
||||
this.state.selectedServer, instance, this.state.includeAll, false
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
||||
<div className="mx_NetworkDropdown_input" onClick={this.onInputClick}>
|
||||
{current_value}
|
||||
<span className="mx_NetworkDropdown_arrow"></span>
|
||||
{menu}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDropdown.propTypes = {
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
protocols: React.PropTypes.object,
|
||||
// The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
|
||||
config: React.PropTypes.object,
|
||||
};
|
||||
|
||||
NetworkDropdown.defaultProps = {
|
||||
protocols: {},
|
||||
config: {},
|
||||
};
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
|
@ -27,12 +30,38 @@ import React from 'react';
|
|||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, ...restProps} = props;
|
||||
restProps.onClick = onClick;
|
||||
// We need to consume enter onKeyDown and space onKeyUp
|
||||
// otherwise we are risking also activating other keyboard focusable elements
|
||||
// that might receive focus as a result of the AccessibleButtonClick action
|
||||
// It's because we are using html buttons at a few places e.g. inside dialogs
|
||||
// And divs which we report as role button to assistive technologies.
|
||||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||
// inconsistencies here
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.onKeyUp = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
"mx_AccessibleButton";
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
|
@ -44,9 +73,9 @@ export default function AccessibleButton(props) {
|
|||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
element: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import AccessibleButton from './AccessibleButton';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoleButton',
|
||||
|
@ -47,6 +48,7 @@ export default React.createClass({
|
|||
|
||||
_onClick: function(ev) {
|
||||
ev.stopPropagation();
|
||||
Analytics.trackEvent('Action Button', 'click', this.props.action);
|
||||
dis.dispatch({action: this.props.action});
|
||||
},
|
||||
|
||||
|
@ -75,10 +77,11 @@ export default React.createClass({
|
|||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||
{tooltip}
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
|
@ -26,15 +27,17 @@ export default React.createClass({
|
|||
displayName: 'AddressSelector',
|
||||
|
||||
propTypes: {
|
||||
onSelected: React.PropTypes.func.isRequired,
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
// Whether to show the address on the address tiles
|
||||
showAddress: PropTypes.bool,
|
||||
truncateAt: PropTypes.number.isRequired,
|
||||
selected: PropTypes.number,
|
||||
|
||||
// Element to put as a header on top of the list
|
||||
header: React.PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -46,8 +49,8 @@ export default React.createClass({
|
|||
|
||||
componentWillReceiveProps: function(props) {
|
||||
// Make sure the selected item isn't outside the list bounds
|
||||
var selected = this.state.selected;
|
||||
var maxSelected = this._maxSelected(props.addressList);
|
||||
const selected = this.state.selected;
|
||||
const maxSelected = this._maxSelected(props.addressList);
|
||||
if (selected > maxSelected) {
|
||||
this.setState({ selected: maxSelected });
|
||||
}
|
||||
|
@ -57,7 +60,7 @@ export default React.createClass({
|
|||
// As the user scrolls with the arrow keys keep the selected item
|
||||
// at the top of the window.
|
||||
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
|
||||
var elementHeight = this.addressListElement.getBoundingClientRect().height;
|
||||
const elementHeight = this.addressListElement.getBoundingClientRect().height;
|
||||
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
|
||||
}
|
||||
},
|
||||
|
@ -75,7 +78,7 @@ export default React.createClass({
|
|||
if (this.state.selected > 0) {
|
||||
this.setState({
|
||||
selected: this.state.selected - 1,
|
||||
hover : false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -84,7 +87,7 @@ export default React.createClass({
|
|||
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
||||
this.setState({
|
||||
selected: this.state.selected + 1,
|
||||
hover : false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -105,7 +108,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
this.setState({ hover : false });
|
||||
this.setState({ hover: false });
|
||||
},
|
||||
|
||||
selectAddress: function(index) {
|
||||
|
@ -117,15 +120,15 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
createAddressListTiles: function() {
|
||||
var self = this;
|
||||
var AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
var maxSelected = this._maxSelected(this.props.addressList);
|
||||
var addressList = [];
|
||||
const self = this;
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
const maxSelected = this._maxSelected(this.props.addressList);
|
||||
const addressList = [];
|
||||
|
||||
// Only create the address elements if there are address
|
||||
if (this.props.addressList.length > 0) {
|
||||
for (var i = 0; i <= maxSelected; i++) {
|
||||
var classes = classNames({
|
||||
for (let i = 0; i <= maxSelected; i++) {
|
||||
const classes = classNames({
|
||||
"mx_AddressSelector_addressListElement": true,
|
||||
"mx_AddressSelector_selected": this.state.selected === i,
|
||||
});
|
||||
|
@ -142,8 +145,14 @@ export default React.createClass({
|
|||
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
||||
ref={(ref) => { this.addressListElement = ref; }}
|
||||
>
|
||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
</div>
|
||||
<AddressTile
|
||||
address={this.props.addressList[i]}
|
||||
showAddress={this.props.showAddress}
|
||||
justified={true}
|
||||
networkName="vector"
|
||||
networkUrl="img/search-icon-vector.svg"
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -151,13 +160,13 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_maxSelected: function(list) {
|
||||
var listSize = list.length === 0 ? 0 : list.length - 1;
|
||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||
const listSize = list.length === 0 ? 0 : list.length - 1;
|
||||
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||
return maxSelected;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var classes = classNames({
|
||||
const classes = classNames({
|
||||
"mx_AddressSelector": true,
|
||||
"mx_AddressSelector_empty": this.props.addressList.length === 0,
|
||||
});
|
||||
|
@ -168,5 +177,5 @@ export default React.createClass({
|
|||
{ this.createAddressListTiles() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
@ -28,9 +29,9 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
address: UserAddressType.isRequired,
|
||||
canDismiss: React.PropTypes.bool,
|
||||
onDismissed: React.PropTypes.func,
|
||||
justified: React.PropTypes.bool,
|
||||
canDismiss: PropTypes.bool,
|
||||
onDismissed: PropTypes.func,
|
||||
justified: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -45,11 +46,12 @@ export default React.createClass({
|
|||
const address = this.props.address;
|
||||
const name = address.displayName || address.address;
|
||||
|
||||
let imgUrls = [];
|
||||
const imgUrls = [];
|
||||
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||
|
||||
if (address.addressType === "mx" && address.avatarMxc) {
|
||||
if (isMatrixAddress && address.avatarMxc) {
|
||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
address.avatarMxc, 25, 25, 'crop',
|
||||
));
|
||||
} else if (address.addressType === 'email') {
|
||||
imgUrls.push('img/icon-email-user.svg');
|
||||
|
@ -77,7 +79,7 @@ export default React.createClass({
|
|||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
if (isMatrixAddress && address.isKnown) {
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -86,10 +88,13 @@ export default React.createClass({
|
|||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={nameClasses}>{ name }</div>
|
||||
<div className={idClasses}>{ address.address }</div>
|
||||
{ this.props.showAddress ?
|
||||
<div className={idClasses}>{ address.address }</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else if (address.addressType === "mx") {
|
||||
} else if (isMatrixAddress) {
|
||||
const unknownMxClasses = classNames({
|
||||
"mx_AddressTile_unknownMx": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -106,24 +111,24 @@ export default React.createClass({
|
|||
|
||||
let nameNode = null;
|
||||
if (address.displayName) {
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>;
|
||||
}
|
||||
|
||||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
{nameNode}
|
||||
{ nameNode }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
var unknownClasses = classNames({
|
||||
const unknownClasses = classNames({
|
||||
"mx_AddressTile_unknown": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
info = (
|
||||
<div className={unknownClasses}>{_t("Unknown Address")}</div>
|
||||
<div className={unknownClasses}>{ _t("Unknown Address") }</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -150,5 +155,5 @@ export default React.createClass({
|
|||
{ dismiss }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
|
||||
export default class AppPermission extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -19,9 +20,9 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if(curl) {
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
curlString = curl.format();
|
||||
}
|
||||
|
@ -33,33 +34,25 @@ export default class AppPermission extends React.Component {
|
|||
return curlString;
|
||||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if(wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
wurl.hostname === 'demo.riot.im' ||
|
||||
wurl.hostname === 'localhost'
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
e2eWarningText =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('NOTE: Apps are not end-to-end encrypted')}</span>;
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||
}
|
||||
const cookieWarning =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>
|
||||
{ _t('Warning: This widget might use cookies.') }
|
||||
</span>;
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
||||
{e2eWarningText}
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
{ e2eWarningText }
|
||||
{ cookieWarning }
|
||||
</div>
|
||||
<input
|
||||
className='mx_AppPermissionButton'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
/**
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,75 +18,115 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
propTypes: {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
url: React.PropTypes.string.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
room: React.PropTypes.object.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: React.PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: React.PropTypes.string,
|
||||
},
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.id;
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url: "",
|
||||
};
|
||||
},
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
getInitialState: function() {
|
||||
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||
* Component props *must* be passed (rather than relying on this.props).
|
||||
* @param {Object} newProps The new properties of the component
|
||||
* @return {Object} Updated component state to be set with setState
|
||||
*/
|
||||
_getNewState(newProps) {
|
||||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
return {
|
||||
loading: false,
|
||||
widgetUrl: this.props.url,
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||
isScalarUrl: function() {
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
/**
|
||||
* Does the widget support a given capability
|
||||
* @param {string} capability Capability to check for
|
||||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
const u = url.parse(urlString);
|
||||
if (!u) {
|
||||
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||
return url;
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (this.props.url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL, minus the hash because that will change when
|
||||
// we view a different room (ie. may change for persistent widgets)
|
||||
params.parentUrl = window.location.href.split('#', 2)[0];
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
isMixedContent: function() {
|
||||
return u.format();
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
const childContentProtocol = u.protocol;
|
||||
|
@ -95,108 +136,322 @@ export default React.createClass({
|
|||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillMount: function() {
|
||||
if (!this.isScalarUrl()) {
|
||||
componentWillMount() {
|
||||
this.setScalarToken();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
|
||||
// postMessaging API
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
* Component initialisation is only complete when this function has resolved
|
||||
*/
|
||||
setScalarToken() {
|
||||
this.setState({initialising: true});
|
||||
|
||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fetch the token before loading the iframe as we need to mangle the URL
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
this._scalarClient = new ScalarAuthClient();
|
||||
|
||||
// Fetch the token before loading the iframe as we need it to mangle the URL
|
||||
if (!this._scalarClient) {
|
||||
this._scalarClient = new ScalarAuthClient();
|
||||
}
|
||||
this._scalarClient.getScalarToken().done((token) => {
|
||||
// Append scalar_token as a query param
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this.props.url);
|
||||
if (!u.search) {
|
||||
u.search = "?scalar_token=" + encodeURIComponent(token);
|
||||
} else {
|
||||
u.search += "&scalar_token=" + encodeURIComponent(token);
|
||||
const u = url.parse(this._addWurlParams(this.props.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: u.format(),
|
||||
loading: false,
|
||||
initialising: false,
|
||||
});
|
||||
|
||||
// Fetch page title from remote content if not already set
|
||||
if (!this.state.widgetPageTitle && params.url) {
|
||||
this._fetchWidgetTitle(params.url);
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to get scalar_token", err);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
loading: false,
|
||||
initialising: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_canUserModify: function() {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
},
|
||||
|
||||
_onEditClick: function(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
_onDeleteClick: function() {
|
||||
if (this._canUserModify()) {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.url !== this.props.url) {
|
||||
this._getNewState(nextProps);
|
||||
this.setScalarToken();
|
||||
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||
this.setState({
|
||||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Legacy Jitsi widget messaging
|
||||
// TODO -- This should be replaced with the new widget postMessaging API
|
||||
_onMessage(event) {
|
||||
if (this.props.type !== 'jitsi') {
|
||||
return;
|
||||
}
|
||||
if (!event.origin) {
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||
const eventOrigin = url.parse(event.origin);
|
||||
if (
|
||||
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||
eventOrigin.host !== widgetUrlObj.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
|
||||
const iframe = this.refs.appFrame.contentWindow
|
||||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||
}
|
||||
}
|
||||
|
||||
_canUserModify() {
|
||||
// User widgets should always be modifiable by their creator
|
||||
if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) {
|
||||
return true;
|
||||
}
|
||||
// Check if the current user can modify widgets in the current room
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
}
|
||||
|
||||
_onEditClick(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
* otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
_onDeleteClick() {
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else {
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
||||
title: _t('Failed to remove widget'),
|
||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
|
||||
this._setupWidgetMessaging();
|
||||
}
|
||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
let requestedWhitelistCapabilies = [];
|
||||
|
||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||
return this.indexOf(e)>=0;
|
||||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_fetchWidgetTitle(url) {
|
||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||
if (widgetPageTitle) {
|
||||
this.setState({widgetPageTitle: widgetPageTitle});
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
_deleteWidgetLabel() {
|
||||
if (this._canUserModify()) {
|
||||
return 'Delete widget';
|
||||
return _td('Delete widget');
|
||||
}
|
||||
return 'Revoke widget access';
|
||||
},
|
||||
return _td('Revoke widget access');
|
||||
}
|
||||
|
||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||
_grantWidgetPermission() {
|
||||
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
},
|
||||
}
|
||||
|
||||
_revokeWidgetPermission() {
|
||||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.removeItem(this.state.widgetPermissionId);
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
},
|
||||
|
||||
formatAppTileName: function() {
|
||||
// Force the widget to be non-persistent
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
if (this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
}
|
||||
|
||||
onClickMenuBar: function(ev) {
|
||||
onClickMenuBar(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Ignore clicks on menu bar children
|
||||
|
@ -209,9 +464,56 @@ export default React.createClass({
|
|||
action: 'appsDrawer',
|
||||
show: !this.props.show,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||
title = this.state.widgetPageTitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<b>{ name }</b>
|
||||
<span>{ title ? titleSpacer : '' }{ title }</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
_onMinimiseClick(e) {
|
||||
if (this.props.onMinimiseClick) {
|
||||
this.props.onMinimiseClick();
|
||||
}
|
||||
}
|
||||
|
||||
_onPopoutWidgetClick(e) {
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes,noreferrer=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick(e) {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
// Don't render widget if it is in the process of being deleted
|
||||
|
@ -226,44 +528,69 @@ export default React.createClass({
|
|||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
|
||||
if (this.props.show) {
|
||||
if (this.state.loading) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
<MessageSpinner msg='Loading...' />
|
||||
</div>
|
||||
);
|
||||
if (this.state.initialising) {
|
||||
appTileBody = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<MessageSpinner msg='Loading...'/>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ loadingElement }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<AppWarning
|
||||
errorMsg="Error - Mixed content"
|
||||
/>
|
||||
<div className={appTileBodyClass}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
"allow" attribute, which is unknown to react 15.
|
||||
*/ }
|
||||
<iframe
|
||||
is
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
src={safeWidgetUrl}
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remian on screen, we must put it in
|
||||
// a PersistedElement from the get-go, otherwise the iframe will be
|
||||
// re-mounted later when we do.
|
||||
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppPermission
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
url={this.state.widgetUrl}
|
||||
|
@ -277,40 +604,167 @@ export default React.createClass({
|
|||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||
let deleteIcon = 'img/cancel.svg';
|
||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||
if(this._canUserModify()) {
|
||||
deleteIcon = 'img/cancel-red.svg';
|
||||
let deleteIcon = 'img/cancel_green.svg';
|
||||
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||
if (this._canUserModify()) {
|
||||
deleteIcon = 'img/icon-delete-pink.svg';
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
// Picture snapshot - only show button when apps are maximised.
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
let appTileClass;
|
||||
if (this.props.miniMode) {
|
||||
appTileClass = 'mx_AppTile_mini';
|
||||
} else if (this.props.fullWidth) {
|
||||
appTileClass = 'mx_AppTileFullWidth';
|
||||
} else {
|
||||
appTileClass = 'mx_AppTile';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
{this.formatAppTileName()}
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ this.props.showMinimise && <TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{/* Edit widget */}
|
||||
{showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
{ /* Reload widget */ }
|
||||
{ this.props.showReload && <TintableSvgButton
|
||||
src={reloadWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Reload widget')}
|
||||
onClick={this._onReloadWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Popout widget */ }
|
||||
{ this.props.showPopout && <TintableSvgButton
|
||||
src={popoutWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Snapshot widget */ }
|
||||
{ showPictureSnapshotButton && <TintableSvgButton
|
||||
src={showPictureSnapshotIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Picture')}
|
||||
onClick={this._onSnapshotClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className={"mx_AppTileMenuBarWidget " +
|
||||
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
/>}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{/* Delete widget */}
|
||||
<img src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
alt={_t(deleteWidgetLabel)}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
/>
|
||||
{ /* Delete widget */ }
|
||||
{ this.props.showDelete && <TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
</span>
|
||||
</div>
|
||||
{appTileBody}
|
||||
</div> }
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AppTile.displayName ='AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: PropTypes.string,
|
||||
waitForIframeLoad: PropTypes.bool,
|
||||
showMenubar: PropTypes.bool,
|
||||
// Should the AppTile render itself
|
||||
show: PropTypes.bool,
|
||||
// Optional onEditClickHandler (overrides default behaviour)
|
||||
onEditClick: PropTypes.func,
|
||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||
onDeleteClick: PropTypes.func,
|
||||
// Optional onMinimiseClickHandler
|
||||
onMinimiseClick: PropTypes.func,
|
||||
// Optionally hide the tile title
|
||||
showTitle: PropTypes.bool,
|
||||
// Optionally hide the tile minimise icon
|
||||
showMinimise: PropTypes.bool,
|
||||
// Optionally handle minimise button pointer events (default false)
|
||||
handleMinimisePointerEvents: PropTypes.bool,
|
||||
// Optionally hide the delete icon
|
||||
showDelete: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Optionally show the reload widget icon
|
||||
// This is not currently intended for use with production widgets. However
|
||||
// it can be useful when developing persistent widgets in order to avoid
|
||||
// having to reload all of riot to get new widget content.
|
||||
showReload: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Optional function to be called on widget capability request
|
||||
// Called with an array of the requested capabilities
|
||||
onCapabilityRequest: PropTypes.func,
|
||||
// Is this an instance of a user widget
|
||||
userWidget: PropTypes.bool,
|
||||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
showMinimise: true,
|
||||
showDelete: true,
|
||||
showPopout: true,
|
||||
showReload: false,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
|
|
@ -6,10 +6,10 @@ const AppWarning = (props) => {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ const CreateRoomButton = function(props) {
|
|||
return (
|
||||
<ActionButton action="view_create_room"
|
||||
mouseOverAction={props.callout ? "callout_create_room" : null}
|
||||
label={ _t("Create new room") }
|
||||
label={_t("Create new room")}
|
||||
iconPath="img/icons-create-room.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
45
src/components/views/elements/DNDTagTile.js
Normal file
45
src/components/views/elements/DNDTagTile.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* eslint new-cap: "off" */
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile {...props} />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>;
|
||||
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,13 +25,13 @@ export default React.createClass({
|
|||
displayName: 'DeviceVerifyButtons',
|
||||
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
device: this.props.device
|
||||
device: this.props.device,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -60,37 +61,37 @@ export default React.createClass({
|
|||
|
||||
onUnverifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
onBlacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, true
|
||||
this.props.userId, this.state.device.deviceId, true,
|
||||
);
|
||||
},
|
||||
|
||||
onUnblacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var blacklistButton = null, verifyButton = null;
|
||||
let blacklistButton = null, verifyButton = null;
|
||||
|
||||
if (this.state.device.isBlocked()) {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
{_t("Unblacklist")}
|
||||
{ _t("Unblacklist") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||
onClick={this.onBlacklistClick}>
|
||||
{_t("Blacklist")}
|
||||
{ _t("Blacklist") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -99,14 +100,14 @@ export default React.createClass({
|
|||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
{_t("Unverify")}
|
||||
{ _t("Unverify") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
{_t("Verify...")}
|
||||
{ _t("Verify...") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
85
src/components/views/elements/DialogButtons.js
Normal file
85
src/components/views/elements/DialogButtons.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2017 Aidan Gauland
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Basic container for buttons in modal dialogs.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "DialogButtons",
|
||||
|
||||
propTypes: {
|
||||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: PropTypes.node.isRequired,
|
||||
|
||||
// A node to insert into the cancel button instead of default "Cancel"
|
||||
cancelButton: PropTypes.node,
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||
|
||||
// should there be a cancel button? default: true
|
||||
hasCancel: PropTypes.bool,
|
||||
|
||||
// onClick handler for the cancel button.
|
||||
onCancel: PropTypes.func,
|
||||
|
||||
focus: PropTypes.bool,
|
||||
|
||||
disabled: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
hasCancel: true,
|
||||
disabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this.props.onCancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let primaryButtonClassName = "mx_Dialog_primary";
|
||||
if (this.props.primaryButtonClass) {
|
||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
}
|
||||
let cancelButton;
|
||||
if (this.props.cancelButton || this.props.hasCancel) {
|
||||
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
||||
{ this.props.cancelButton || _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={primaryButtonClassName}
|
||||
onClick={this.props.onPrimaryButtonClick}
|
||||
autoFocus={this.props.focus}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{ this.props.primaryButton }
|
||||
</button>
|
||||
{ this.props.children }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class DirectorySearchBox extends React.Component {
|
||||
|
@ -95,7 +96,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||
placeholder={this.props.placeholder} autoFocus
|
||||
/>
|
||||
{join_button}
|
||||
{ join_button }
|
||||
<span className="mx_DirectorySearchBox_clear_wrapper">
|
||||
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||
</span>
|
||||
|
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
|
|||
}
|
||||
|
||||
DirectorySearchBox.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
onClear: React.PropTypes.func,
|
||||
onJoinClick: React.PropTypes.func,
|
||||
placeholder: React.PropTypes.string,
|
||||
showJoinButton: React.PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
onJoinClick: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
showJoinButton: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -26,6 +27,10 @@ class MenuOption extends React.Component {
|
|||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
_onMouseEnter() {
|
||||
this.props.onMouseEnter(this.props.dropdownKey);
|
||||
}
|
||||
|
@ -46,20 +51,20 @@ class MenuOption extends React.Component {
|
|||
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
React.PropTypes.node
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(React.PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
highlighted: React.PropTypes.bool,
|
||||
dropdownKey: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
onMouseEnter: React.PropTypes.func.isRequired,
|
||||
highlighted: PropTypes.bool,
|
||||
dropdownKey: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -153,6 +158,8 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
_onInputClick(ev) {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
if (!this.state.expanded) {
|
||||
this.setState({
|
||||
expanded: true,
|
||||
|
@ -250,13 +257,13 @@ export default class Dropdown extends React.Component {
|
|||
onMouseEnter={this._setHighlightedOption}
|
||||
onClick={this._onMenuOptionClick}
|
||||
>
|
||||
{child}
|
||||
{ child }
|
||||
</MenuOption>
|
||||
);
|
||||
});
|
||||
if (options.length === 0) {
|
||||
return [<div key="0" className="mx_Dropdown_option">
|
||||
{_t("No results")}
|
||||
{ _t("No results") }
|
||||
</div>];
|
||||
}
|
||||
return options;
|
||||
|
@ -279,7 +286,7 @@ export default class Dropdown extends React.Component {
|
|||
/>;
|
||||
}
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{this._getMenuOptions()}
|
||||
{ this._getMenuOptions() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -288,12 +295,13 @@ export default class Dropdown extends React.Component {
|
|||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
currentValue = <div className="mx_Dropdown_option">
|
||||
{selectedChild}
|
||||
</div>
|
||||
{ selectedChild }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const dropdownClasses = {
|
||||
mx_Dropdown: true,
|
||||
mx_Dropdown_disabled: this.props.disabled,
|
||||
};
|
||||
if (this.props.className) {
|
||||
dropdownClasses[this.props.className] = true;
|
||||
|
@ -303,9 +311,9 @@ export default class Dropdown extends React.Component {
|
|||
// to the input, but overflows below it. The root contains both.
|
||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
|
||||
{currentValue}
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow"></span>
|
||||
{menu}
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
@ -315,18 +323,20 @@ Dropdown.propTypes = {
|
|||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
// Called when the selected option changes
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
searchEnabled: React.PropTypes.bool,
|
||||
onSearchChange: PropTypes.func,
|
||||
searchEnabled: PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
}
|
||||
getShortOption: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
// negative for consistency with HTML
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
|
156
src/components/views/elements/EditableItemList.js
Normal file
156
src/components/views/elements/EditableItemList.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler.js';
|
||||
|
||||
const EditableItem = React.createClass({
|
||||
displayName: 'EditableItem',
|
||||
|
||||
propTypes: {
|
||||
initialValue: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onAdd: PropTypes.func,
|
||||
|
||||
addOnChange: PropTypes.bool,
|
||||
},
|
||||
|
||||
onChange: function(value) {
|
||||
this.setState({ value });
|
||||
if (this.props.onChange) this.props.onChange(value, this.props.index);
|
||||
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
|
||||
},
|
||||
|
||||
onRemove: function() {
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
},
|
||||
|
||||
onAdd: function() {
|
||||
if (this.props.onAdd) this.props.onAdd(this.state.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return <div className="mx_EditableItem">
|
||||
<EditableText
|
||||
className="mx_EditableItem_editable"
|
||||
placeholderClassName="mx_EditableItem_editablePlaceholder"
|
||||
placeholder={this.props.placeholder}
|
||||
blurToCancel={false}
|
||||
editable={true}
|
||||
initialValue={this.props.initialValue}
|
||||
onValueChanged={this.onChange} />
|
||||
{ this.props.onAdd ?
|
||||
<div className="mx_EditableItem_addButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src="img/plus.svg" width="14" height="14"
|
||||
alt={_t("Add")} onClick={this.onAdd} />
|
||||
</div>
|
||||
:
|
||||
<div className="mx_EditableItem_removeButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src="img/cancel-small.svg" width="14" height="14"
|
||||
alt={_t("Delete")} onClick={this.onRemove} />
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableItemList',
|
||||
|
||||
propTypes: {
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemEdited: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onItemAdded: () => {},
|
||||
onItemEdited: () => {},
|
||||
onItemRemoved: () => {},
|
||||
onNewItemChanged: () => {},
|
||||
};
|
||||
},
|
||||
|
||||
onItemAdded: function(value) {
|
||||
this.props.onItemAdded(value);
|
||||
},
|
||||
|
||||
onItemEdited: function(value, index) {
|
||||
if (value.length === 0) {
|
||||
this.onItemRemoved(index);
|
||||
} else {
|
||||
this.props.onItemEdited(value, index);
|
||||
}
|
||||
},
|
||||
|
||||
onItemRemoved: function(index) {
|
||||
this.props.onItemRemoved(index);
|
||||
},
|
||||
|
||||
onNewItemChanged: function(value) {
|
||||
this.props.onNewItemChanged(value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const editableItems = this.props.items.map((item, index) => {
|
||||
return <EditableItem
|
||||
key={index}
|
||||
index={index}
|
||||
initialValue={item}
|
||||
onChange={this.onItemEdited}
|
||||
onRemove={this.onItemRemoved}
|
||||
placeholder={this.props.placeholder}
|
||||
/>;
|
||||
});
|
||||
|
||||
const label = this.props.items.length > 0 ?
|
||||
this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// This is slightly evil; we want a new instance of
|
||||
// EditableItem when the list grows. To make sure it's
|
||||
// reset to its initial state.
|
||||
<EditableItem
|
||||
key={editableItems.length}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
addOnChange={true}
|
||||
placeholder={this.props.placeholder}
|
||||
/> : <div />
|
||||
}
|
||||
</div>);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,30 +15,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
labelClassName: React.PropTypes.string,
|
||||
placeholderClassName: React.PropTypes.string,
|
||||
onValueChanged: PropTypes.func,
|
||||
initialValue: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
placeholderClassName: PropTypes.string,
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel: React.PropTypes.bool,
|
||||
blurToCancel: PropTypes.bool,
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
},
|
||||
|
||||
Phases: {
|
||||
|
@ -93,8 +89,7 @@ module.exports = React.createClass({
|
|||
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
||||
this.placeholder = true;
|
||||
this.value = '';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.refs.editable_div.textContent = this.value;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
|
@ -137,7 +132,7 @@ module.exports = React.createClass({
|
|||
this.showPlaceholder(false);
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
@ -150,14 +145,13 @@ module.exports = React.createClass({
|
|||
|
||||
if (!ev.target.textContent) {
|
||||
this.showPlaceholder(true);
|
||||
}
|
||||
else if (!this.placeholder) {
|
||||
} else if (!this.placeholder) {
|
||||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
} else if (ev.key === "Escape") {
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
|
@ -175,24 +169,24 @@ module.exports = React.createClass({
|
|||
onFocus: function(ev) {
|
||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||
|
||||
var node = ev.target.childNodes[0];
|
||||
const node = ev.target.childNodes[0];
|
||||
if (node) {
|
||||
var range = document.createRange();
|
||||
const range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
|
||||
var sel = window.getSelection();
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
},
|
||||
|
||||
onFinish: function(ev, shouldSubmit) {
|
||||
var self = this;
|
||||
var submit = (ev.key === "Enter") || shouldSubmit;
|
||||
const self = this;
|
||||
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
}, () => {
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
}
|
||||
|
@ -200,29 +194,38 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var sel = window.getSelection();
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel)
|
||||
{this.cancelEdit();}
|
||||
else
|
||||
{this.onFinish(ev, this.props.blurToSubmit);}
|
||||
if (this.props.blurToCancel) {
|
||||
this.cancelEdit();
|
||||
} else {
|
||||
this.onFinish(ev, this.props.blurToSubmit);
|
||||
}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var editable_el;
|
||||
const {className, editable, initialValue, label, labelClassName} = this.props;
|
||||
let editableEl;
|
||||
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
||||
{ label || initialValue }
|
||||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
editableEl = <div ref="editable_div"
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />;
|
||||
}
|
||||
|
||||
return editable_el;
|
||||
}
|
||||
return editableEl;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
|
@ -64,7 +65,7 @@ export default class EditableTextContainer extends React.Component {
|
|||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -96,22 +97,22 @@ export default class EditableTextContainer extends React.Component {
|
|||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.busy) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else if (this.state.errorString) {
|
||||
return (
|
||||
<div className="error">{this.state.errorString}</div>
|
||||
<div className="error">{ this.state.errorString }</div>
|
||||
);
|
||||
} else {
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText initialValue={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
|
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
|
|||
|
||||
EditableTextContainer.propTypes = {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue: React.PropTypes.func,
|
||||
getInitialValue: PropTypes.func,
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue: React.PropTypes.string,
|
||||
initialValue: PropTypes.string,
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder: React.PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: React.PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
|
@ -32,8 +33,8 @@ export default function EmojiText(props) {
|
|||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
element: React.PropTypes.string,
|
||||
children: React.PropTypes.string.isRequired,
|
||||
element: PropTypes.string,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiText.defaultProps = {
|
||||
|
|
142
src/components/views/elements/Flair.js
Normal file
142
src/components/views/elements/Flair.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
ev.preventDefault();
|
||||
// Don't trigger onClick of parent element
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupProfile.groupId,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
this.props.groupProfile.groupId;
|
||||
return <img
|
||||
src={httpUrl}
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick}
|
||||
title={tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
FlairAvatar.propTypes = {
|
||||
groupProfile: PropTypes.shape({
|
||||
groupId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
avatarUrl: PropTypes.string.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
FlairAvatar.contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
export default class Flair extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
profiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._generateAvatars(this.props.groups);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this._generateAvatars(newProps.groups);
|
||||
}
|
||||
|
||||
async _getGroupProfiles(groups) {
|
||||
const profiles = [];
|
||||
for (const groupId of groups) {
|
||||
let groupProfile = null;
|
||||
try {
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
|
||||
} catch (err) {
|
||||
console.error('Could not get profile for group', groupId, err);
|
||||
}
|
||||
profiles.push(groupProfile);
|
||||
}
|
||||
return profiles.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
async _generateAvatars(groups) {
|
||||
if (!groups || groups.length === 0) {
|
||||
return;
|
||||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
profiles: profiles.filter((profile) => {
|
||||
return profile ? profile.avatarUrl : false;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.profiles.length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
const avatars = this.state.profiles.map((profile, index) => {
|
||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||
});
|
||||
return (
|
||||
<span className="mx_Flair">
|
||||
{ avatars }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Flair.propTypes = {
|
||||
groups: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
||||
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||
Flair.contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
35
src/components/views/elements/GeminiScrollbarWrapper.js
Normal file
35
src/components/views/elements/GeminiScrollbarWrapper.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function GeminiScrollbarWrapper(props) {
|
||||
const {wrappedRef, ...wrappedProps} = props;
|
||||
|
||||
// Enable forceGemini so that gemini is always enabled. This is
|
||||
// to avoid future issues where a feature is implemented without
|
||||
// doing QA on every OS/browser combination.
|
||||
//
|
||||
// By default GeminiScrollbar allows native scrollbars to be used
|
||||
// on macOS. Use forceGemini to enable Gemini's non-native
|
||||
// scrollbars on all OSs.
|
||||
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
|
||||
{ props.children }
|
||||
</GeminiScrollbar>;
|
||||
}
|
||||
export default GeminiScrollbarWrapper;
|
||||
|
39
src/components/views/elements/GroupsButton.js
Normal file
39
src/components/views/elements/GroupsButton.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_my_groups"
|
||||
label={_t("Communities")}
|
||||
iconPath="img/icons-groups.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GroupsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default GroupsButton;
|
|
@ -23,7 +23,7 @@ const HomeButton = function(props) {
|
|||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_home_page"
|
||||
label={ _t("Home") }
|
||||
label={_t("Home")}
|
||||
iconPath="img/icons-home.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
205
src/components/views/elements/ImageView.js
Normal file
205
src/components/views/elements/ImageView.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
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 React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
var filesize = require('filesize');
|
||||
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ImageView',
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired, // the source of the image being displayed
|
||||
name: React.PropTypes.string, // the main title ('name') for the image
|
||||
link: React.PropTypes.string, // the link (if any) applied to the name of the image
|
||||
width: React.PropTypes.number, // width of the image src in pixels
|
||||
height: React.PropTypes.number, // height of the image src in pixels
|
||||
fileSize: React.PropTypes.number, // size of the image src in bytes
|
||||
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: React.PropTypes.object,
|
||||
},
|
||||
|
||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
||||
// dialog base class somehow, surely...
|
||||
componentDidMount: function() {
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode == 27) { // escape
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
onRedactClick: function() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getName: function () {
|
||||
var name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
/*
|
||||
// In theory max-width: 80%, max-height: 80% on the CSS should work
|
||||
// but in practice, it doesn't, so do it manually:
|
||||
|
||||
var width = this.props.width || 500;
|
||||
var height = this.props.height || 500;
|
||||
|
||||
var maxWidth = document.documentElement.clientWidth * 0.8;
|
||||
var maxHeight = document.documentElement.clientHeight * 0.8;
|
||||
|
||||
var widthFrac = width / maxWidth;
|
||||
var heightFrac = height / maxHeight;
|
||||
|
||||
var displayWidth;
|
||||
var displayHeight;
|
||||
if (widthFrac > heightFrac) {
|
||||
displayWidth = Math.min(width, maxWidth);
|
||||
displayHeight = (displayWidth / width) * height;
|
||||
} else {
|
||||
displayHeight = Math.min(height, maxHeight);
|
||||
displayWidth = (displayHeight / height) * width;
|
||||
}
|
||||
|
||||
var style = {
|
||||
width: displayWidth,
|
||||
height: displayHeight
|
||||
};
|
||||
*/
|
||||
var style, res;
|
||||
|
||||
if (this.props.width && this.props.height) {
|
||||
style = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
};
|
||||
res = style.width + "x" + style.height + "px";
|
||||
}
|
||||
|
||||
var size;
|
||||
if (this.props.fileSize) {
|
||||
size = filesize(this.props.fileSize);
|
||||
}
|
||||
|
||||
var size_res;
|
||||
if (size && res) {
|
||||
size_res = size + ", " + res;
|
||||
}
|
||||
else {
|
||||
size_res = size || res;
|
||||
}
|
||||
|
||||
var showEventMeta = !!this.props.mxEvent;
|
||||
|
||||
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">
|
||||
{ _t('Uploaded on %(date)s by %(user)s', {date: formatDate(new Date(this.props.mxEvent.getTs())), user: sender}) }
|
||||
</div>);
|
||||
}
|
||||
|
||||
var eventRedact;
|
||||
if(showEventMeta) {
|
||||
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ImageView">
|
||||
<div className="mx_ImageView_lhs">
|
||||
</div>
|
||||
<div className="mx_ImageView_content">
|
||||
<img src={this.props.src} style={style}/>
|
||||
<div className="mx_ImageView_labelWrapper">
|
||||
<div className="mx_ImageView_label">
|
||||
<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 } download={ this.props.name } target="_blank" rel="noopener">
|
||||
<div className="mx_ImageView_download">
|
||||
{ _t('Download this file') }<br/>
|
||||
<span className="mx_ImageView_size">{ size_res }</span>
|
||||
</div>
|
||||
</a>
|
||||
{ eventRedact }
|
||||
<div className="mx_ImageView_shim">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_ImageView_rhs">
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -16,10 +16,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
|
@ -35,14 +36,14 @@ export default class LanguageDropdown extends React.Component {
|
|||
this.state = {
|
||||
searchQuery: '',
|
||||
langs: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b){
|
||||
if(a.label < b.label) return -1;
|
||||
if(a.label > b.label) return 1;
|
||||
langs.sort(function(a, b) {
|
||||
if (a.label < b.label) return -1;
|
||||
if (a.label > b.label) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.setState({langs});
|
||||
|
@ -54,10 +55,10 @@ export default class LanguageDropdown extends React.Component {
|
|||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
this.props.onOptionChange(_localSettings.language);
|
||||
}else {
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
} else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
|
@ -89,18 +90,18 @@ export default class LanguageDropdown extends React.Component {
|
|||
|
||||
const options = displayedLanguages.map((language) => {
|
||||
return <div key={language.value}>
|
||||
{language.label}
|
||||
{ language.label }
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
value = this.props.value || _localSettings.language;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
const language = navigator.language || navigator.userLanguage;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
|
@ -108,13 +109,13 @@ export default class LanguageDropdown extends React.Component {
|
|||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true} value={value}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
||||
|
||||
LanguageDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
|
@ -31,11 +32,9 @@ export default class ManageIntegsButton extends React.Component {
|
|||
|
||||
this.state = {
|
||||
scalarError: null,
|
||||
showIntegrationsError: false,
|
||||
};
|
||||
|
||||
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
||||
this.onShowIntegrationsError = this.onShowIntegrationsError.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -48,7 +47,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({ scalarError: err});
|
||||
console.error(err);
|
||||
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -59,53 +58,44 @@ export default class ManageIntegsButton extends React.Component {
|
|||
|
||||
onManageIntegrations(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
return;
|
||||
}
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||
null,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
|
||||
onShowIntegrationsError(ev) {
|
||||
ev.preventDefault();
|
||||
this.setState({
|
||||
showIntegrationsError: !this.state.showIntegrationsError,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
let integrationsError;
|
||||
let integrationsWarningTriangle = <div />;
|
||||
let integrationsErrorPopup = <div />;
|
||||
if (this.scalarClient !== null) {
|
||||
if (this.state.showIntegrationsError && this.state.scalarError) {
|
||||
integrationsError = (
|
||||
const integrationsButtonClasses = classNames({
|
||||
mx_RoomHeader_button: true,
|
||||
mx_RoomSettings_integrationsButton_error: !!this.state.scalarError,
|
||||
});
|
||||
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
integrationsWarningTriangle = <img src="img/warning.svg" title={_t('Integrations Error')} width="17" />;
|
||||
// Popup shown when hovering over integrationsButton_error (via CSS)
|
||||
integrationsErrorPopup = (
|
||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||
{ _t('Could not connect to the integration server') }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onManageIntegrations} title={ _t('Manage Integrations') }>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.state.scalarError) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
<img src="img/warning.svg" title={_t('Integrations Error')} width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onManageIntegrations} title={ _t('Manage Integrations') }>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
integrationsButton = (
|
||||
<AccessibleButton className={integrationsButtonClasses} onClick={this.onManageIntegrations} title={_t('Manage Integrations')}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35" />
|
||||
{ integrationsWarningTriangle }
|
||||
{ integrationsErrorPopup }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return integrationsButton;
|
||||
|
@ -113,5 +103,5 @@ export default class ManageIntegsButton extends React.Component {
|
|||
}
|
||||
|
||||
ManageIntegsButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -23,22 +24,24 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// An array of member events to summarise
|
||||
events: React.PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: React.PropTypes.array.isRequired,
|
||||
children: PropTypes.array.isRequired,
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength: React.PropTypes.number,
|
||||
summaryLength: PropTypes.number,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: React.PropTypes.number,
|
||||
avatarsMaxLength: PropTypes.number,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: React.PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
onToggle: PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
expanded: false,
|
||||
expanded: Boolean(this.props.startExpanded),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -84,7 +87,6 @@ module.exports = React.createClass({
|
|||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this._renderNameList(userNames);
|
||||
const plural = userNames.length > 1;
|
||||
|
||||
const splitTransitions = transitions.split(',');
|
||||
|
||||
|
@ -94,18 +96,18 @@ module.exports = React.createClass({
|
|||
// Transform into consecutive repetitions of the same transition (like 5
|
||||
// consecutive 'joined_and_left's)
|
||||
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||
canonicalTransitions
|
||||
canonicalTransitions,
|
||||
);
|
||||
|
||||
const descs = coalescedTransitions.map((t) => {
|
||||
return this._getDescriptionForTransition(
|
||||
t.transitionType, plural, t.repeats
|
||||
t.transitionType, userNames.length, t.repeats,
|
||||
);
|
||||
});
|
||||
|
||||
const desc = this._renderCommaSeparatedList(descs);
|
||||
|
||||
return nameList + " " + desc;
|
||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
||||
});
|
||||
|
||||
if (!summaries) {
|
||||
|
@ -117,7 +119,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
<EmojiText>
|
||||
{summaries.join(", ")}
|
||||
{ summaries.join(", ") }
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
|
@ -206,148 +208,75 @@ module.exports = React.createClass({
|
|||
* For a certain transition, t, describe what happened to the users that
|
||||
* underwent the transition.
|
||||
* @param {string} t the transition type.
|
||||
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||
* transition.
|
||||
* @param {integer} userCount number of usernames
|
||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
_getDescriptionForTransition(t, plural, repeats) {
|
||||
_getDescriptionForTransition(t, userCount, repeats) {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
let res = null;
|
||||
switch(t) {
|
||||
switch (t) {
|
||||
case "joined":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined", { severalUsers: "" })
|
||||
: _t("%(oneUser)sjoined", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "left":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft", { severalUsers: "" })
|
||||
: _t("%(oneUser)sleft", { oneUser: "" });
|
||||
}
|
||||
break;
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "joined_and_left":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined and left", { severalUsers: "" })
|
||||
: _t("%(oneUser)sjoined and left", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "left_and_joined":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" })
|
||||
: _t("%(oneUser)sleft and rejoined", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invite_reject":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)srejected their invitations", { severalUsers: "" })
|
||||
: _t("%(oneUser)srejected their invitation", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invite_withdrawal":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" })
|
||||
: _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invited":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were invited %(repeats)s times", { repeats: repeats })
|
||||
: _t("was invited %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were invited")
|
||||
: _t("was invited");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were invited %(count)s times", { count: repeats })
|
||||
: _t("was invited %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "banned":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were banned %(repeats)s times", { repeats: repeats })
|
||||
: _t("was banned %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were banned")
|
||||
: _t("was banned");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were banned %(count)s times", { count: repeats })
|
||||
: _t("was banned %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "unbanned":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were unbanned %(repeats)s times", { repeats: repeats })
|
||||
: _t("was unbanned %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were unbanned")
|
||||
: _t("was unbanned");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were unbanned %(count)s times", { count: repeats })
|
||||
: _t("was unbanned %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "kicked":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were kicked %(repeats)s times", { repeats: repeats })
|
||||
: _t("was kicked %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were kicked")
|
||||
: _t("was kicked");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were kicked %(count)s times", { count: repeats })
|
||||
: _t("was kicked %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "changed_name":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their name", { severalUsers: "" })
|
||||
: _t("%(oneUser)schanged their name", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "changed_avatar":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their avatar", { severalUsers: "" })
|
||||
: _t("%(oneUser)schanged their avatar", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -368,17 +297,15 @@ module.exports = React.createClass({
|
|||
*/
|
||||
_renderCommaSeparatedList(items, itemLimit) {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
} else if (items.length === 1) {
|
||||
return items[0];
|
||||
} else if (remaining) {
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return (remaining > 1)
|
||||
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
|
||||
: _t("%(items)s and one other", { items: items.join(', ') });
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
|
@ -392,8 +319,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return (
|
||||
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
|
||||
{avatars}
|
||||
<span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
|
||||
{ avatars }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
@ -417,19 +344,15 @@ module.exports = React.createClass({
|
|||
case 'join':
|
||||
if (e.mxEvent.getPrevContent().membership === 'join') {
|
||||
if (e.mxEvent.getContent().displayname !==
|
||||
e.mxEvent.getPrevContent().displayname)
|
||||
{
|
||||
e.mxEvent.getPrevContent().displayname) {
|
||||
return 'changed_name';
|
||||
}
|
||||
else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url)
|
||||
{
|
||||
} else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url) {
|
||||
return 'changed_avatar';
|
||||
}
|
||||
// console.log("MELS ignoring duplicate membership join event");
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return 'joined';
|
||||
}
|
||||
case 'leave':
|
||||
|
@ -481,7 +404,7 @@ module.exports = React.createClass({
|
|||
firstEvent.index < aggregateIndices[seq]) {
|
||||
aggregateIndices[seq] = firstEvent.index;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -492,7 +415,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const eventsToRender = this.props.events;
|
||||
const eventIds = eventsToRender.map(e => e.getId()).join(',');
|
||||
const eventIds = eventsToRender.map((e) => e.getId()).join(',');
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
|
@ -504,7 +427,7 @@ module.exports = React.createClass({
|
|||
if (fewEvents) {
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{expandedEvents}
|
||||
{ expandedEvents }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -540,7 +463,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Sort types by order of lowest event index within sequence
|
||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
|
||||
);
|
||||
|
||||
let summaryContainer = null;
|
||||
|
@ -548,24 +471,24 @@ module.exports = React.createClass({
|
|||
summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
{this._renderAvatars(avatarMembers)}
|
||||
{this._renderSummary(aggregate.names, orderedTransitionSequences)}
|
||||
{ this._renderAvatars(avatarMembers) }
|
||||
{ this._renderSummary(aggregate.names, orderedTransitionSequences) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const toggleButton = (
|
||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{toggleButton}
|
||||
{summaryContainer}
|
||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||
{expandedEvents}
|
||||
{ toggleButton }
|
||||
{ summaryContainer }
|
||||
{ expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null }
|
||||
{ expandedEvents }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -26,8 +26,8 @@ module.exports = React.createClass({
|
|||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{msg}</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
172
src/components/views/elements/PersistedElement.js
Normal file
172
src/components/views/elements/PersistedElement.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
function getContainer(containerId) {
|
||||
return document.getElementById(containerId);
|
||||
}
|
||||
|
||||
function getOrCreateContainer(containerId) {
|
||||
let container = getContainer(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/*
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
*
|
||||
* This prevents the children from being unmounted when the parent of PersistedElement
|
||||
* unmounts, allowing them to persist.
|
||||
*
|
||||
* When PE is unmounted, it hides the children using CSS. When mounted or updated, the
|
||||
* children are made visible and are positioned into a div that is given the same
|
||||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
this.collectChild = this.collectChild.bind(this);
|
||||
this._repositionChild = this._repositionChild.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
||||
// Annoyingly, a resize observer is insufficient, since we also care
|
||||
// about when the element moves on the screen without changing its
|
||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||
// for this, so we bodge it by listening for document resize and
|
||||
// the timeline_resize action.
|
||||
window.addEventListener('resize', this._repositionChild);
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DOM elements created when a PersistedElement with the given
|
||||
* persistKey was mounted. The DOM elements will be re-added if another
|
||||
* PeristedElement is mounted in the future.
|
||||
*
|
||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||
*/
|
||||
static destroyElement(persistKey) {
|
||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
static isMounted(persistKey) {
|
||||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||
}
|
||||
|
||||
collectChildContainer(ref) {
|
||||
if (this.childContainer) {
|
||||
this.resizeObserver.unobserve(this.childContainer);
|
||||
}
|
||||
this.childContainer = ref;
|
||||
if (ref) {
|
||||
this.resizeObserver.observe(ref);
|
||||
}
|
||||
}
|
||||
|
||||
collectChild(ref) {
|
||||
this.child = ref;
|
||||
this.updateChild();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateChild();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateChild();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.updateChildVisibility(this.child, false);
|
||||
this.resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', this._repositionChild);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === 'timeline_resize') {
|
||||
this._repositionChild();
|
||||
}
|
||||
}
|
||||
|
||||
_repositionChild() {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
}
|
||||
|
||||
updateChild() {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
this.updateChildVisibility(this.child, true);
|
||||
}
|
||||
|
||||
updateChildVisibility(child, visible) {
|
||||
if (!child) return;
|
||||
child.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateChildPosition(child, parent) {
|
||||
if (!child || !parent) return;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
Object.assign(child.style, {
|
||||
position: 'absolute',
|
||||
top: parentRect.top + 'px',
|
||||
left: parentRect.left + 'px',
|
||||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
96
src/components/views/elements/PersistentApp.js
Normal file
96
src/components/views/elements/PersistentApp.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PersistentApp',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(payload) {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
_onActiveWidgetStoreUpdate: function() {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
|
||||
);
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,17 +18,18 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||
|
||||
const Pill = React.createClass({
|
||||
statics: {
|
||||
|
@ -37,11 +39,21 @@ const Pill = React.createClass({
|
|||
isMessagePillUrl: (url) => {
|
||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||
},
|
||||
roomNotifPos: (text) => {
|
||||
return text.indexOf("@room");
|
||||
},
|
||||
roomNotifLen: () => {
|
||||
return "@room".length;
|
||||
},
|
||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
|
||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||
},
|
||||
|
||||
props: {
|
||||
// The Type of this Pill. If url is given, this is auto-detected.
|
||||
type: PropTypes.string,
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
|
@ -50,6 +62,19 @@ const Pill = React.createClass({
|
|||
room: PropTypes.instanceOf(Room),
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar: PropTypes.bool,
|
||||
// Whether to render this pill as if it were highlit by a selection
|
||||
isSelected: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -61,33 +86,47 @@ const Pill = React.createClass({
|
|||
|
||||
// The member related to the user pill
|
||||
member: null,
|
||||
// The group related to the group pill
|
||||
group: null,
|
||||
// The room related to the room pill
|
||||
room: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
let regex = REGEX_MATRIXTO;
|
||||
if (nextProps.inMessage) {
|
||||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
}
|
||||
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = regex.exec(nextProps.url) || [];
|
||||
let matrixToMatch;
|
||||
let resourceId;
|
||||
let prefix;
|
||||
|
||||
const resourceId = matrixToMatch[1]; // The room/user ID
|
||||
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||
if (nextProps.url) {
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
matrixToMatch = regex.exec(nextProps.url) || [];
|
||||
|
||||
const pillType = {
|
||||
resourceId = matrixToMatch[1]; // The room/user ID
|
||||
prefix = matrixToMatch[2]; // The first character of prefix
|
||||
}
|
||||
|
||||
const pillType = this.props.type || {
|
||||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
'+': Pill.TYPE_GROUP_MENTION,
|
||||
}[prefix];
|
||||
|
||||
let member;
|
||||
let group;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
room = nextProps.room;
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
const localMember = nextProps.room.getMember(resourceId);
|
||||
member = localMember;
|
||||
|
@ -110,12 +149,26 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
group = {
|
||||
groupId: resourceId,
|
||||
avatarUrl: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({resourceId, pillType, member, room});
|
||||
this.setState({resourceId, pillType, member, group, room});
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this.componentWillReceiveProps(this.props);
|
||||
},
|
||||
|
||||
|
@ -134,6 +187,9 @@ const Pill = React.createClass({
|
|||
getContent: () => {
|
||||
return {avatar_url: resp.avatar_url};
|
||||
},
|
||||
getDirectionalContent: function() {
|
||||
return this.getContent();
|
||||
},
|
||||
};
|
||||
this.setState({member});
|
||||
}).catch((err) => {
|
||||
|
@ -148,6 +204,7 @@ const Pill = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
|
@ -160,14 +217,26 @@ const Pill = React.createClass({
|
|||
let href = this.props.url;
|
||||
let onClick;
|
||||
switch (this.state.pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
const room = this.props.room;
|
||||
if (room) {
|
||||
linkText = "@room";
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_AtRoomPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
const member = this.state.member;
|
||||
if (member) {
|
||||
userId = member.userId;
|
||||
member.rawDisplayName = member.rawDisplayName || '';
|
||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_UserPill';
|
||||
href = null;
|
||||
|
@ -180,27 +249,42 @@ const Pill = React.createClass({
|
|||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const {avatarUrl, groupId, name} = this.state.group;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const classes = classNames(pillClass, {
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||
"mx_UserPill_selected": this.props.isSelected,
|
||||
});
|
||||
|
||||
if (this.state.pillType) {
|
||||
return this.props.inMessage ?
|
||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</a> :
|
||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</span>;
|
||||
} else {
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue