Merge remote-tracking branch 'origin/experimental' into travis/develop-for-real
This commit is contained in:
commit
8017f0a4a1
164 changed files with 4652 additions and 2772 deletions
137
src/components/structures/AutoHideScrollbar.js
Normal file
137
src/components/structures/AutoHideScrollbar.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
// derived from code from github.com/noeldelgado/gemini-scrollbar
|
||||
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
|
||||
function getScrollbarWidth(alternativeOverflow) {
|
||||
const div = document.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = '-9999px';
|
||||
div.style.width = '100px';
|
||||
div.style.height = '100px';
|
||||
div.style.overflow = "scroll";
|
||||
if (alternativeOverflow) {
|
||||
div.style.overflow = alternativeOverflow;
|
||||
}
|
||||
div.style.msOverflowStyle = '-ms-autohiding-scrollbar';
|
||||
document.body.appendChild(div);
|
||||
const scrollbarWidth = (div.offsetWidth - div.clientWidth);
|
||||
document.body.removeChild(div);
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
function install() {
|
||||
const scrollbarWidth = getScrollbarWidth();
|
||||
if (scrollbarWidth !== 0) {
|
||||
const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0;
|
||||
// overflow: overlay on webkit doesn't auto hide the scrollbar
|
||||
if (hasForcedOverlayScrollbar) {
|
||||
document.body.classList.add("mx_scrollbar_overlay_noautohide");
|
||||
} else {
|
||||
document.body.classList.add("mx_scrollbar_nooverlay");
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.innerText =
|
||||
`body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const installBodyClassesIfNeeded = (function() {
|
||||
let installed = false;
|
||||
return function() {
|
||||
if (!installed) {
|
||||
install();
|
||||
installed = true;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
export default class AutoHideScrollbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onOverflow = this.onOverflow.bind(this);
|
||||
this.onUnderflow = this.onUnderflow.bind(this);
|
||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
||||
this._needsOverflowListener = null;
|
||||
}
|
||||
|
||||
onOverflow() {
|
||||
this.containerRef.classList.add("mx_AutoHideScrollbar_overflow");
|
||||
this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow");
|
||||
}
|
||||
|
||||
onUnderflow() {
|
||||
this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow");
|
||||
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
if (!this._needsOverflowListener) {
|
||||
return;
|
||||
}
|
||||
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
|
||||
this.onOverflow();
|
||||
} else {
|
||||
this.onUnderflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
installBodyClassesIfNeeded();
|
||||
this._needsOverflowListener =
|
||||
document.body.classList.contains("mx_scrollbar_nooverlay");
|
||||
if (this._needsOverflowListener) {
|
||||
this.containerRef.addEventListener("overflow", this.onOverflow);
|
||||
this.containerRef.addEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
_collectContainerRef(ref) {
|
||||
if (ref && !this.containerRef) {
|
||||
this.containerRef = ref;
|
||||
}
|
||||
if (this.props.wrappedRef) {
|
||||
this.props.wrappedRef(ref);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._needsOverflowListener && this.containerRef) {
|
||||
this.containerRef.removeEventListener("overflow", this.onOverflow);
|
||||
this.containerRef.removeEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div
|
||||
ref={this._collectContainerRef}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
>
|
||||
<div className="mx_AutoHideScrollbar_offset">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ export default class ContextualMenu extends React.Component {
|
|||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
|
@ -113,7 +113,6 @@ export default class ContextualMenu extends React.Component {
|
|||
render() {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
||||
const props = this.props;
|
||||
|
||||
if (props.top) {
|
||||
|
@ -137,6 +136,8 @@ export default class ContextualMenu extends React.Component {
|
|||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
const hasChevron = chevronFace && chevronFace !== "none";
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
|
@ -174,11 +175,14 @@ export default class ContextualMenu extends React.Component {
|
|||
`;
|
||||
}
|
||||
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||
const chevron = hasChevron ?
|
||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||
undefined;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_noChevron': chevronFace === 'none',
|
||||
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||
|
|
|
@ -1,284 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
const PresetValues = {
|
||||
PrivateChat: "private_chat",
|
||||
PublicChat: "public_chat",
|
||||
Custom: "custom",
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoom',
|
||||
|
||||
propTypes: {
|
||||
onRoomCreated: PropTypes.func,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
phases: {
|
||||
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
||||
CREATING: "CREATING", // We're sending the request.
|
||||
CREATED: "CREATED", // We successfully created the room.
|
||||
ERROR: "ERROR", // There was an error while trying to create room.
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onRoomCreated: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.phases.CONFIG,
|
||||
error_string: "",
|
||||
is_private: true,
|
||||
share_history: false,
|
||||
default_preset: PresetValues.PrivateChat,
|
||||
topic: '',
|
||||
room_name: '',
|
||||
invited_users: [],
|
||||
};
|
||||
},
|
||||
|
||||
onCreateRoom: function() {
|
||||
const options = {};
|
||||
|
||||
if (this.state.room_name) {
|
||||
options.name = this.state.room_name;
|
||||
}
|
||||
|
||||
if (this.state.topic) {
|
||||
options.topic = this.state.topic;
|
||||
}
|
||||
|
||||
if (this.state.preset) {
|
||||
if (this.state.preset != PresetValues.Custom) {
|
||||
options.preset = this.state.preset;
|
||||
} else {
|
||||
options.initial_state = [
|
||||
{
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
"join_rule": this.state.is_private ? "invite" : "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
content: {
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
options.invite = this.state.invited_users;
|
||||
|
||||
const alias = this.getAliasLocalpart();
|
||||
if (alias) {
|
||||
options.room_alias_name = alias;
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
// TODO: Error.
|
||||
console.error("Cannot create room: No matrix client.");
|
||||
return;
|
||||
}
|
||||
|
||||
const deferred = cli.createRoom(options);
|
||||
|
||||
if (this.state.encrypt) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.CREATING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
||||
deferred.then(function(resp) {
|
||||
self.setState({
|
||||
phase: self.phases.CREATED,
|
||||
});
|
||||
self.props.onRoomCreated(resp.room_id);
|
||||
}, function(err) {
|
||||
self.setState({
|
||||
phase: self.phases.ERROR,
|
||||
error_string: err.toString(),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getPreset: function() {
|
||||
return this.refs.presets.getPreset();
|
||||
},
|
||||
|
||||
getName: function() {
|
||||
return this.refs.name_textbox.getName();
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.topic.getTopic();
|
||||
},
|
||||
|
||||
getAliasLocalpart: function() {
|
||||
return this.refs.alias.getAliasLocalpart();
|
||||
},
|
||||
|
||||
getInvitedUsers: function() {
|
||||
return this.refs.user_selector.getUserIds();
|
||||
},
|
||||
|
||||
onPresetChanged: function(preset) {
|
||||
switch (preset) {
|
||||
case PresetValues.PrivateChat:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
is_private: true,
|
||||
share_history: false,
|
||||
});
|
||||
break;
|
||||
case PresetValues.PublicChat:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
is_private: false,
|
||||
share_history: true,
|
||||
});
|
||||
break;
|
||||
case PresetValues.Custom:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onPrivateChanged: function(ev) {
|
||||
this.setState({
|
||||
preset: PresetValues.Custom,
|
||||
is_private: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
onShareHistoryChanged: function(ev) {
|
||||
this.setState({
|
||||
preset: PresetValues.Custom,
|
||||
share_history: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
onTopicChange: function(ev) {
|
||||
this.setState({
|
||||
topic: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onNameChange: function(ev) {
|
||||
this.setState({
|
||||
room_name: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onInviteChanged: function(invited_users) {
|
||||
this.setState({
|
||||
invited_users: invited_users,
|
||||
});
|
||||
},
|
||||
|
||||
onAliasChanged: function(alias) {
|
||||
this.setState({
|
||||
alias: alias,
|
||||
});
|
||||
},
|
||||
|
||||
onEncryptChanged: function(ev) {
|
||||
this.setState({
|
||||
encrypt: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const curr_phase = this.state.phase;
|
||||
if (curr_phase == this.phases.CREATING) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else {
|
||||
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}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
return (
|
||||
<div className="mx_CreateRoom">
|
||||
<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 />
|
||||
<div>
|
||||
<label>
|
||||
<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') }
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_CreateRoom_encrypt">
|
||||
<label>
|
||||
<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 }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -24,6 +24,9 @@ import dis from '../../dispatcher';
|
|||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import GroupHeaderButtons from '../views/right_panel/GroupHeaderButtons';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import Modal from '../../Modal';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -1271,25 +1274,19 @@ export default React.createClass({
|
|||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
if (this.props.collapsedRhs) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button"
|
||||
onClick={this._onShowRhsClick} title={_t('Show panel')} key="_maximiseButton"
|
||||
>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
|
||||
|
||||
const headerClasses = {
|
||||
mx_GroupView_header: true,
|
||||
mx_GroupView_header_view: !this.state.editing,
|
||||
mx_GroupView_header_isUserMember: this.state.isUserMember,
|
||||
"mx_GroupView_header": true,
|
||||
"light-panel": true,
|
||||
"mx_GroupView_header_view": !this.state.editing,
|
||||
"mx_GroupView_header_isUserMember": this.state.isUserMember,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx_GroupView">
|
||||
<main className="mx_GroupView">
|
||||
<div className={classnames(headerClasses)}>
|
||||
<div className="mx_GroupView_header_leftCol">
|
||||
<div className="mx_GroupView_header_avatar">
|
||||
|
@ -1307,12 +1304,15 @@ export default React.createClass({
|
|||
<div className="mx_GroupView_header_rightCol">
|
||||
{ rightButtons }
|
||||
</div>
|
||||
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
||||
</div>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</MainSplit>
|
||||
</main>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
if (this.state.error.httpStatus === 404) {
|
||||
|
|
73
src/components/structures/IndicatorScrollbar.js
Normal file
73
src/components/structures/IndicatorScrollbar.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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 AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default class IndicatorScrollbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._collectScroller = this._collectScroller.bind(this);
|
||||
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
|
||||
this.checkOverflow = this.checkOverflow.bind(this);
|
||||
this._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
}
|
||||
|
||||
_collectScroller(scroller) {
|
||||
if (scroller && !this._scrollElement) {
|
||||
this._scrollElement = scroller;
|
||||
this._scrollElement.addEventListener("scroll", this.checkOverflow);
|
||||
this.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
_collectScrollerComponent(autoHideScrollbar) {
|
||||
this._autoHideScrollbar = autoHideScrollbar;
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
||||
if (hasTopOverflow) {
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||
} else {
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||
}
|
||||
if (hasBottomOverflow) {
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||
} else {
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||
}
|
||||
|
||||
if (this._autoHideScrollbar) {
|
||||
this._autoHideScrollbar.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._scrollElement) {
|
||||
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<AutoHideScrollbar ref={this._collectScrollerComponent} wrappedRef={this._collectScroller} {... this.props}>
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>);
|
||||
}
|
||||
}
|
|
@ -151,8 +151,7 @@ const LeftPanel = React.createClass({
|
|||
}
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_SearchBox_search") ||
|
||||
classes.contains("mx_RoomSubList_ellipsis")));
|
||||
classes.contains("mx_textinput_search")));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
|
@ -171,6 +170,12 @@ const LeftPanel = React.createClass({
|
|||
this.setState({ searchFilter: term });
|
||||
},
|
||||
|
||||
onSearchCleared: function(source) {
|
||||
if (source === "keyboard") {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
},
|
||||
|
||||
collectRoomList: function(ref) {
|
||||
this._roomList = ref;
|
||||
},
|
||||
|
@ -178,18 +183,9 @@ const LeftPanel = React.createClass({
|
|||
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');
|
||||
|
||||
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
|
||||
const classes = classNames(
|
||||
"mx_LeftPanel",
|
||||
{
|
||||
"collapsed": this.props.collapsed,
|
||||
},
|
||||
);
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
const tagPanelEnabled = !SettingsStore.getValue("TagPanel.disableTagPanel");
|
||||
const tagPanel = tagPanelEnabled ? <TagPanel /> : <div />;
|
||||
|
@ -197,27 +193,32 @@ const LeftPanel = React.createClass({
|
|||
const containerClasses = classNames(
|
||||
"mx_LeftPanel_container", "mx_fadable",
|
||||
{
|
||||
"mx_LeftPanel_container_collapsed": this.props.collapsed,
|
||||
"collapsed": this.props.collapsed,
|
||||
"mx_LeftPanel_container_hasTagPanel": tagPanelEnabled,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
},
|
||||
);
|
||||
|
||||
const searchBox = !this.props.collapsed ?
|
||||
<SearchBox onSearch={ this.onSearch } onCleared={ this.onSearchCleared } /> :
|
||||
undefined;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanel }
|
||||
<aside className={classes} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
{ topBox }
|
||||
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
<TopLeftMenuButton collapsed={ this.props.collapsed } />
|
||||
{ searchBox }
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<RoomList
|
||||
ref={this.collectRoomList}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
<BottomLeftMenu collapsed={this.props.collapsed} />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
// <BottomLeftMenu collapsed={this.props.collapsed}/>
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ import RoomListStore from "../../stores/RoomListStore";
|
|||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, CollapseDistributor} from '../../resizer'
|
||||
// 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.
|
||||
|
@ -61,7 +62,7 @@ const LoggedInView = React.createClass({
|
|||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
collapsedRhs: PropTypes.bool,
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// Used by the RoomView to handle joining rooms
|
||||
|
@ -94,6 +95,12 @@ const LoggedInView = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
@ -123,6 +130,7 @@ const LoggedInView = React.createClass({
|
|||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
this.resizer.detach();
|
||||
},
|
||||
|
||||
// Child components assume that the client peg will not be null, so give them some
|
||||
|
@ -148,6 +156,39 @@ const LoggedInView = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_createResizer() {
|
||||
const classNames = {
|
||||
handle: "mx_ResizeHandle",
|
||||
vertical: "mx_ResizeHandle_vertical",
|
||||
reverse: "mx_ResizeHandle_reverse"
|
||||
};
|
||||
const collapseConfig = {
|
||||
toggleSize: 260 - 50,
|
||||
onCollapsed: (collapsed) => {
|
||||
this.setState({collapseLhs: collapsed});
|
||||
if (collapsed) {
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
}
|
||||
},
|
||||
onResized: (size) => {
|
||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
},
|
||||
};
|
||||
const resizer = new Resizer(
|
||||
this.resizeContainer,
|
||||
CollapseDistributor,
|
||||
collapseConfig);
|
||||
resizer.setClassNames(classNames);
|
||||
return resizer;
|
||||
},
|
||||
|
||||
_loadResizerPreferences() {
|
||||
const lhsSize = window.localStorage.getItem("mx_lhs_size");
|
||||
if (lhsSize !== null) {
|
||||
this.resizer.forHandleAt(0).resize(parseInt(lhsSize, 10));
|
||||
}
|
||||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "im.vector.web.settings") {
|
||||
this.setState({
|
||||
|
@ -364,12 +405,14 @@ const LoggedInView = React.createClass({
|
|||
this.setState({mouseDown: null});
|
||||
},
|
||||
|
||||
_setResizeContainerRef(div) {
|
||||
this.resizeContainer = div;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
const HomePage = sdk.getComponent('structures.HomePage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
|
@ -382,7 +425,6 @@ const LoggedInView = React.createClass({
|
|||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
|
@ -396,12 +438,9 @@ const LoggedInView = React.createClass({
|
|||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
/>;
|
||||
if (!this.props.collapseRhs) {
|
||||
right_panel = <RightPanel roomId={this.props.currentRoomId} disabled={this.props.rightDisabled} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
|
@ -411,21 +450,12 @@ const LoggedInView = React.createClass({
|
|||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
page_element = <MyGroups />;
|
||||
break;
|
||||
|
||||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
onRoomCreated={this.props.onRoomCreated}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.RoomDirectory:
|
||||
page_element = <RoomDirectory
|
||||
ref="roomDirectory"
|
||||
|
@ -451,15 +481,15 @@ const LoggedInView = React.createClass({
|
|||
|
||||
case PageTypes.UserView:
|
||||
page_element = null; // deliberately null for now
|
||||
right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
// TODO: fix/remove UserView
|
||||
// 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}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -511,15 +541,13 @@ const LoggedInView = React.createClass({
|
|||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
collapsed={this.props.collapseLhs || this.state.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
<ResizeHandle/>
|
||||
{ page_element }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
|
|
101
src/components/structures/MainSplit.js
Normal file
101
src/components/structures/MainSplit.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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 ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, FixedDistributor} from '../../resizer';
|
||||
|
||||
export default class MainSplit extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._setResizeContainerRef = this._setResizeContainerRef.bind(this);
|
||||
this._onResized = this._onResized.bind(this);
|
||||
}
|
||||
|
||||
_onResized(size) {
|
||||
window.localStorage.setItem("mx_rhs_size", size);
|
||||
}
|
||||
|
||||
_createResizer() {
|
||||
const classNames = {
|
||||
handle: "mx_ResizeHandle",
|
||||
vertical: "mx_ResizeHandle_vertical",
|
||||
reverse: "mx_ResizeHandle_reverse",
|
||||
};
|
||||
const resizer = new Resizer(
|
||||
this.resizeContainer,
|
||||
FixedDistributor,
|
||||
{onResized: this._onResized},
|
||||
);
|
||||
resizer.setClassNames(classNames);
|
||||
let rhsSize = window.localStorage.getItem("mx_rhs_size");
|
||||
if (rhsSize !== null) {
|
||||
rhsSize = parseInt(rhsSize, 10);
|
||||
} else {
|
||||
rhsSize = 350;
|
||||
}
|
||||
resizer.forHandleAt(0).resize(rhsSize);
|
||||
|
||||
resizer.attach();
|
||||
this.resizer = resizer;
|
||||
}
|
||||
|
||||
_setResizeContainerRef(div) {
|
||||
this.resizeContainer = div;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.panel && !this.props.collapsedRhs) {
|
||||
this._createResizer();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.resizer) {
|
||||
this.resizer.detach();
|
||||
this.resizer = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
|
||||
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
|
||||
const wasPanelSet = this.props.panel && !prevProps.panel;
|
||||
const wasPanelCleared = !this.props.panel && prevProps.panel;
|
||||
|
||||
if (wasExpanded || wasPanelSet) {
|
||||
this._createResizer();
|
||||
} else if (wasCollapsed || wasPanelCleared) {
|
||||
this.resizer.detach();
|
||||
this.resizer = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const bodyView = React.Children.only(this.props.children);
|
||||
const panelView = this.props.panel;
|
||||
|
||||
if (this.props.collapsedRhs || !panelView) {
|
||||
return bodyView;
|
||||
} else {
|
||||
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
|
||||
{ bodyView }
|
||||
<ResizeHandle reverse={true} />
|
||||
{ panelView }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -163,7 +163,7 @@ export default React.createClass({
|
|||
viewUserId: null,
|
||||
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
|
||||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
rightDisabled: false,
|
||||
|
@ -579,7 +579,7 @@ export default React.createClass({
|
|||
break;
|
||||
case 'view_user':
|
||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||
if (this.state.collapseRhs) {
|
||||
if (this.state.collapsedRhs) {
|
||||
setTimeout(()=>{
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
|
@ -680,13 +680,15 @@ export default React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", true);
|
||||
this.setState({
|
||||
collapseRhs: true,
|
||||
collapsedRhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", false);
|
||||
this.setState({
|
||||
collapseRhs: false,
|
||||
collapsedRhs: false,
|
||||
});
|
||||
break;
|
||||
case 'panel_disable': {
|
||||
|
@ -697,9 +699,11 @@ export default React.createClass({
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
// case 'set_theme':
|
||||
// disable changing the theme for now
|
||||
// as other themes are not compatible with dharma
|
||||
// this._onSetTheme(payload.value);
|
||||
// break;
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
|
@ -1239,7 +1243,7 @@ export default React.createClass({
|
|||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
collapsedRhs: false,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
|
|
|
@ -631,12 +631,20 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_scrollDownIfAtBottom: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let topSpinner;
|
||||
let bottomSpinner;
|
||||
|
@ -656,6 +664,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
|
||||
let whoIsTyping;
|
||||
if (this.props.room) {
|
||||
whoIsTyping = (<WhoIsTypingTile room={this.props.room} onVisible={this._scrollDownIfAtBottom} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
|
@ -666,6 +679,7 @@ module.exports = React.createClass({
|
|||
stickyBottom={this.props.stickyBottom}>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
);
|
||||
|
|
|
@ -60,7 +60,6 @@ export default withMatrixClient(React.createClass({
|
|||
render: function() {
|
||||
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");
|
||||
|
||||
|
@ -112,7 +111,6 @@ export default withMatrixClient(React.createClass({
|
|||
<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>
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector 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,86 +20,28 @@ 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);
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
Analytics.trackEvent(...this.props.analytics);
|
||||
dis.dispatch({
|
||||
action: 'view_right_panel_phase',
|
||||
phase: this.props.clickPhase,
|
||||
});
|
||||
static get contextTypes() {
|
||||
return {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
return <AccessibleButton
|
||||
aria-label={this.props.title}
|
||||
aria-expanded={this.props.isHighlighted}
|
||||
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: {
|
||||
static Phase = Object.freeze({
|
||||
RoomMemberList: 'RoomMemberList',
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
|
@ -107,160 +50,102 @@ module.exports = React.createClass({
|
|||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
},
|
||||
});
|
||||
|
||||
componentWillMount: function() {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
phase: this.props.groupId ? RightPanel.Phase.GroupMemberList : RightPanel.Phase.RoomMemberList,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
||||
|
||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context.matrixClient;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
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() {
|
||||
onGroupStoreUpdated() {
|
||||
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() {
|
||||
onInviteToGroupButtonClick() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: this.Phase.GroupMemberList,
|
||||
phase: RightPanel.Phase.GroupMemberList,
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onAddRoomToGroupButtonClick: function() {
|
||||
onAddRoomToGroupButtonClick() {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
onRoomStateMember(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) {
|
||||
if (this.state.phase === RightPanel.Phase.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
this._delayedUpdate();
|
||||
} else if (this.state.phase === this.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
|
||||
} else if (this.state.phase === RightPanel.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") {
|
||||
onAction(payload) {
|
||||
if (payload.action === "view_right_panel_phase") {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
groupRoomId: payload.groupRoomId,
|
||||
groupId: payload.groupId,
|
||||
member: payload.member,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
|
@ -271,155 +156,42 @@ module.exports = React.createClass({
|
|||
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(
|
||||
<AccessibleButton 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" alt="" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) {
|
||||
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
|
||||
} else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) {
|
||||
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||
panel = <GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) {
|
||||
panel = <GroupRoomInfo
|
||||
groupRoomId={this.state.groupRoomId}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.groupRoomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
|
||||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
"collapsed": this.props.collapsed,
|
||||
"mx_fadable_faded": this.props.disabled,
|
||||
"dark-panel": true,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,10 +62,6 @@ module.exports = React.createClass({
|
|||
// more interesting)
|
||||
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: PropTypes.number,
|
||||
|
||||
// true if the room is being peeked at. This affects components that shouldn't
|
||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||
isPeeking: PropTypes.bool,
|
||||
|
@ -103,24 +99,16 @@ module.exports = React.createClass({
|
|||
onVisible: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
whoIsTypingLimit: 3,
|
||||
};
|
||||
},
|
||||
|
||||
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();
|
||||
|
@ -135,7 +123,6 @@ module.exports = React.createClass({
|
|||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("sync", this.onSyncStateChange);
|
||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
}
|
||||
},
|
||||
|
@ -150,12 +137,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
|
@ -199,7 +180,6 @@ module.exports = React.createClass({
|
|||
// indicate other sizes.
|
||||
_getSize: function() {
|
||||
if (this._shouldShowConnectionError() ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall ||
|
||||
|
@ -213,10 +193,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
//
|
||||
// if wantPlaceholder is true, we include a "..." placeholder if
|
||||
// there is nothing better to put in.
|
||||
_getIndicator: function(wantPlaceholder) {
|
||||
_getIndicator: function() {
|
||||
if (this.props.numUnreadMessages) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
|
@ -250,49 +227,9 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (wantPlaceholder) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||
{ this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
_renderTypingIndicatorAvatars: function(limit) {
|
||||
let users = this.state.usersTyping;
|
||||
|
||||
let othersCount = 0;
|
||||
if (users.length > limit) {
|
||||
othersCount = users.length - limit + 1;
|
||||
users = users.slice(0, limit - 1);
|
||||
}
|
||||
|
||||
const avatars = users.map((u) => {
|
||||
return (
|
||||
<MemberAvatar
|
||||
key={u.userId}
|
||||
member={u}
|
||||
width={24}
|
||||
height={24}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||
+{ othersCount }
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return avatars;
|
||||
},
|
||||
|
||||
_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)
|
||||
|
@ -440,18 +377,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
this.state.usersTyping,
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
if (typingString) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingBar">
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.hasActiveCall) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_callBar">
|
||||
|
@ -483,7 +408,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const content = this._getContent();
|
||||
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
const indicator = this._getIndicator();
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
|
|
|
@ -19,12 +19,11 @@ 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 IndicatorScrollbar from './IndicatorScrollbar';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -33,8 +32,6 @@ 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',
|
||||
|
||||
|
@ -44,7 +41,6 @@ const RoomSubList = React.createClass({
|
|||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
|
@ -55,21 +51,15 @@ const RoomSubList = React.createClass({
|
|||
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
|
||||
isFiltered: PropTypes.bool,
|
||||
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: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -77,18 +67,12 @@ const RoomSubList = React.createClass({
|
|||
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);
|
||||
},
|
||||
|
||||
|
@ -96,23 +80,6 @@ const RoomSubList = React.createClass({
|
|||
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() {
|
||||
|
@ -143,15 +110,9 @@ const RoomSubList = React.createClass({
|
|||
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);
|
||||
this.setState({hidden: isHidden}, () => {
|
||||
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);
|
||||
|
@ -178,10 +139,9 @@ const RoomSubList = React.createClass({
|
|||
/**
|
||||
* 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) {
|
||||
roomNotificationCount: function() {
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
|
@ -189,20 +149,18 @@ const RoomSubList = React.createClass({
|
|||
}
|
||||
|
||||
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 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;
|
||||
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;
|
||||
}
|
||||
if (badges) {
|
||||
result[0] += notificationCount;
|
||||
if (highlight) {
|
||||
result[1] = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -217,15 +175,9 @@ const RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
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
|
||||
return this.props.list.map((room, index) => {
|
||||
return <RoomTile
|
||||
room={room}
|
||||
roomSubList={this}
|
||||
tagName={this.props.tagName}
|
||||
|
@ -246,7 +198,7 @@ const RoomSubList = React.createClass({
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.state.sortedList) {
|
||||
for (const room of this.props.list) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
@ -269,10 +221,10 @@ const RoomSubList = React.createClass({
|
|||
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) {
|
||||
if (this.props.list && this.props.list.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.sortedList[0].roomId,
|
||||
room_id: this.props.list[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
|
||||
|
@ -287,32 +239,25 @@ const RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
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>;
|
||||
if (!this.props.collapsed) {
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
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
|
||||
|
@ -320,9 +265,6 @@ const RoomSubList = React.createClass({
|
|||
let title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
title += " [" + roomCount + "]";
|
||||
}
|
||||
}
|
||||
|
||||
let incomingCall;
|
||||
|
@ -333,126 +275,77 @@ const RoomSubList = React.createClass({
|
|||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
|
||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
let addRoomButton;
|
||||
if (this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
|
||||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
let chevron;
|
||||
if (len) {
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
chevron = (<div className={chevronClasses}></div>);
|
||||
}
|
||||
|
||||
const tabindex = this.props.isFiltered ? "0" : "-1";
|
||||
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}
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ chevron }
|
||||
<span>{this.props.label}</span>
|
||||
{ incomingCall }
|
||||
</AccessibleButton>
|
||||
{ badge }
|
||||
{ addRoomButton }
|
||||
</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);
|
||||
checkOverflow: function() {
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.checkOverflow();
|
||||
}
|
||||
|
||||
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;
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
if (len) {
|
||||
const subListClasses = classNames({
|
||||
"mx_RoomSubList": true,
|
||||
"mx_RoomSubList_hidden": this.state.hidden,
|
||||
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
|
||||
});
|
||||
if (this.state.hidden) {
|
||||
return <div className={subListClasses}>
|
||||
{this._getHeaderJsx()}
|
||||
</div>;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
const tiles = this.makeRoomTiles();
|
||||
tiles.push(...this.props.extraTiles);
|
||||
return <div className={subListClasses}>
|
||||
{this._getHeaderJsx()}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
}
|
||||
} 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) {
|
||||
let content;
|
||||
if (this.props.showSpinner && !this.state.hidden) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||
{ this.state.hidden ? undefined : content }
|
||||
{ this._getHeaderJsx() }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,8 @@ const Rooms = require('../../Rooms');
|
|||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
|
@ -1571,18 +1573,20 @@ module.exports = React.createClass({
|
|||
oobData={this.props.oobData}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
|
@ -1616,16 +1620,18 @@ module.exports = React.createClass({
|
|||
room={this.state.room}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
room={this.state.room}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
|
@ -1668,7 +1674,6 @@ module.exports = React.createClass({
|
|||
onResize={this.onChildResize}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
whoIsTypingLimit={3}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -1732,6 +1737,7 @@ module.exports = React.createClass({
|
|||
|
||||
const auxPanel = (
|
||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||
fullHeight={this.state.editingRoomSettings}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
|
@ -1860,14 +1866,10 @@ module.exports = React.createClass({
|
|||
let topUnreadMessagesBar = null;
|
||||
if (this.state.showTopUnreadMessagesBar) {
|
||||
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
|
||||
topUnreadMessagesBar = (
|
||||
<div className="mx_RoomView_topUnreadMessagesBar">
|
||||
<TopUnreadMessagesBar
|
||||
onScrollUpClick={this.jumpToReadMarker}
|
||||
onCloseClick={this.forgetReadMarker}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
topUnreadMessagesBar = (<TopUnreadMessagesBar
|
||||
onScrollUpClick={this.jumpToReadMarker}
|
||||
onCloseClick={this.forgetReadMarker}
|
||||
/>);
|
||||
}
|
||||
const statusBarAreaClass = classNames(
|
||||
"mx_RoomView_statusArea",
|
||||
|
@ -1883,8 +1885,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
|
||||
const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} /> : undefined;
|
||||
|
||||
return (
|
||||
<div className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
editing={this.state.editingRoomSettings}
|
||||
|
@ -1899,20 +1903,24 @@ module.exports = React.createClass({
|
|||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
/>
|
||||
{ auxPanel }
|
||||
<div className={fadableSectionClasses}>
|
||||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{ statusBar }
|
||||
<MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}>
|
||||
<div className={fadableSectionClasses}>
|
||||
{ auxPanel }
|
||||
<div className="mx_RoomView_timeline">
|
||||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{ statusBar }
|
||||
</div>
|
||||
</div>
|
||||
{ messageComposer }
|
||||
</div>
|
||||
{ messageComposer }
|
||||
</div>
|
||||
</div>
|
||||
</MainSplit>
|
||||
</main>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,8 +28,8 @@ module.exports = React.createClass({
|
|||
displayName: 'SearchBox',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool,
|
||||
onSearch: React.PropTypes.func,
|
||||
onCleared: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -56,7 +56,6 @@ module.exports = React.createClass({
|
|||
case 'focus_room_filter':
|
||||
if (this.refs.search) {
|
||||
this.refs.search.focus();
|
||||
this.refs.search.select();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -75,86 +74,49 @@ module.exports = React.createClass({
|
|||
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'});
|
||||
this._clearSearch("keyboard");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_clearSearch: function() {
|
||||
_onFocus: function(ev) {
|
||||
ev.target.select();
|
||||
},
|
||||
|
||||
_clearSearch: function(source) {
|
||||
this.refs.search.value = "";
|
||||
this.onChange();
|
||||
if (this.props.onCleared) {
|
||||
this.props.onCleared(source);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
const collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
|
||||
const clearButton = this.state.searchTerm.length > 0 ?
|
||||
(<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button")} }>
|
||||
</AccessibleButton>) : undefined;
|
||||
|
||||
let 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>;
|
||||
}
|
||||
|
||||
let 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') }
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox">
|
||||
{ searchControls }
|
||||
{ toggleCollapse }
|
||||
<div className="mx_SearchBox mx_textinput">
|
||||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
ref="search"
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
/>
|
||||
{ clearButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ import GroupActions from '../../actions/GroupActions';
|
|||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
@ -47,6 +48,8 @@ const TagPanel = React.createClass({
|
|||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
|
@ -67,6 +70,9 @@ const TagPanel = React.createClass({
|
|||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership() {
|
||||
|
@ -100,13 +106,21 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createDialog(RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
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 ActionButton = sdk.getComponent("elements.ActionButton");
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
|
@ -162,7 +176,10 @@ const TagPanel = React.createClass({
|
|||
</GeminiScrollbarWrapper>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_groupsButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
<GroupsButton />
|
||||
<ActionButton
|
||||
className="mx_TagPanel_report" action="show_redesign_feedback_dialog"
|
||||
label={_t("Report bugs & give feedback")} tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -33,9 +33,13 @@ const ObjectUtils = require('../../ObjectUtils');
|
|||
const Modal = require("../../Modal");
|
||||
const UserActivity = require("../../UserActivity");
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
const READ_MARKER_INVIEW_THRESHOLD_MS = 1 * 1000;
|
||||
const READ_MARKER_OUTOFVIEW_THRESHOLD_MS = 30 * 1000;
|
||||
const READ_RECEIPT_INTERVAL_MS = 500;
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
|
@ -188,6 +192,14 @@ var TimelinePanel = React.createClass({
|
|||
this.lastRRSentEventId = undefined;
|
||||
this.lastRMSentEventId = undefined;
|
||||
|
||||
if (this.props.manageReadReceipts) {
|
||||
this.updateReadReceiptOnUserActivity();
|
||||
}
|
||||
if (this.props.manageReadMarkers) {
|
||||
this.updateReadMarkerOnUserActivity();
|
||||
}
|
||||
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
||||
|
@ -254,6 +266,14 @@ var TimelinePanel = React.createClass({
|
|||
//
|
||||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
if (this._readReceiptActivityTimer) {
|
||||
this._readReceiptActivityTimer.abort();
|
||||
this._readReceiptActivityTimer = null;
|
||||
}
|
||||
if (this._readMarkerActivityTimer) {
|
||||
this._readMarkerActivityTimer.abort();
|
||||
this._readMarkerActivityTimer = null;
|
||||
}
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
|
@ -362,30 +382,25 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
const rmPosition = this.getReadMarkerPosition();
|
||||
// we hide the read marker when it first comes onto the screen, but if
|
||||
// it goes back off the top of the screen (presumably because the user
|
||||
// clicks on the 'jump to bottom' button), we need to re-enable it.
|
||||
if (this.getReadMarkerPosition() < 0) {
|
||||
if (rmPosition < 0) {
|
||||
this.setState({readMarkerVisible: true});
|
||||
}
|
||||
|
||||
// if read marker position goes between 0 and -1/1,
|
||||
// (and user is active), switch timeout
|
||||
const timeout = this._readMarkerTimeout(rmPosition);
|
||||
// NO-OP when timeout already has set to the given value
|
||||
this._readMarkerActivityTimer.changeTimeout(timeout);
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'user_activity':
|
||||
case 'user_activity_end':
|
||||
// we could treat user_activity_end differently and not
|
||||
// send receipts for messages that have arrived between
|
||||
// the actual user activity and the time they stopped
|
||||
// being active, but let's see if this is actually
|
||||
// necessary.
|
||||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
break;
|
||||
case 'ignore_state_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
if (payload.action === 'ignore_state_changed') {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -531,6 +546,38 @@ var TimelinePanel = React.createClass({
|
|||
this.setState({clientSyncState: state});
|
||||
},
|
||||
|
||||
_readMarkerTimeout(readMarkerPosition) {
|
||||
return readMarkerPosition === 0 ?
|
||||
READ_MARKER_INVIEW_THRESHOLD_MS :
|
||||
READ_MARKER_OUTOFVIEW_THRESHOLD_MS;
|
||||
},
|
||||
|
||||
updateReadMarkerOnUserActivity: async function() {
|
||||
const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition());
|
||||
this._readMarkerActivityTimer = new Timer(initialTimeout);
|
||||
|
||||
while (this._readMarkerActivityTimer) { //unset on unmount
|
||||
UserActivity.timeWhileActive(this._readMarkerActivityTimer);
|
||||
try {
|
||||
await this._readMarkerActivityTimer.finished();
|
||||
} catch(e) { continue; /* aborted */ }
|
||||
// outside of try/catch to not swallow errors
|
||||
this.updateReadMarker();
|
||||
}
|
||||
},
|
||||
|
||||
updateReadReceiptOnUserActivity: async function() {
|
||||
this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
|
||||
while (this._readReceiptActivityTimer) { //unset on unmount
|
||||
UserActivity.timeWhileActive(this._readReceiptActivityTimer);
|
||||
try {
|
||||
await this._readReceiptActivityTimer.finished();
|
||||
} catch(e) { continue; /* aborted */ }
|
||||
// outside of try/catch to not swallow errors
|
||||
this.sendReadReceipt();
|
||||
}
|
||||
},
|
||||
|
||||
sendReadReceipt: function() {
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this.props.manageReadReceipts) return;
|
||||
|
@ -634,10 +681,11 @@ var TimelinePanel = React.createClass({
|
|||
// of the screen, so move the marker down to the bottom of the screen.
|
||||
updateReadMarker: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
if (this.getReadMarkerPosition() !== 0) {
|
||||
if (this.getReadMarkerPosition() === 1) {
|
||||
// the read marker is at an event below the viewport,
|
||||
// we don't want to rewind it.
|
||||
return;
|
||||
}
|
||||
|
||||
// move the RM to *after* the message at the bottom of the screen. This
|
||||
// avoids a problem whereby we never advance the RM if there is a huge
|
||||
// message which doesn't fit on the screen.
|
||||
|
@ -654,7 +702,6 @@ var TimelinePanel = React.createClass({
|
|||
if (lastDisplayedIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs());
|
||||
|
@ -749,7 +796,6 @@ var TimelinePanel = React.createClass({
|
|||
this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
|
||||
},
|
||||
|
||||
|
||||
/* update the read-up-to marker to match the read receipt
|
||||
*/
|
||||
forgetReadMarker: function() {
|
||||
|
@ -822,15 +868,12 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
canJumpToReadMarker: function() {
|
||||
// 1. Do not show jump bar if neither the RM nor the RR are set.
|
||||
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
|
||||
// read messages and unread messages. We already have a badge count and the bottom
|
||||
// bar to jump to "live" when we have unread messages.
|
||||
// 3. We want to show the bar if the read-marker is off the top of the screen.
|
||||
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
|
||||
const pos = this.getReadMarkerPosition();
|
||||
return this.state.readMarkerEventId !== null && // 1.
|
||||
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
|
||||
const ret = this.state.readMarkerEventId !== null && // 1.
|
||||
(pos < 0 || pos === null); // 3., 4.
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -917,7 +960,6 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
|
||||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1154,6 +1196,7 @@ var TimelinePanel = React.createClass({
|
|||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
room={this.props.timelineSet.room}
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
|
|
115
src/components/structures/TopLeftMenuButton.js
Normal file
115
src/components/structures/TopLeftMenuButton.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
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 * as ContextualMenu from './ContextualMenu';
|
||||
import {TopLeftMenu} from '../views/context_menus/TopLeftMenu';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import Avatar from '../../Avatar';
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
export default class TopLeftMenuButton extends React.Component {
|
||||
static propTypes = {
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static displayName = 'TopLeftMenuButton';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
profileInfo: null,
|
||||
};
|
||||
|
||||
this.onToggleMenu = this.onToggleMenu.bind(this);
|
||||
}
|
||||
|
||||
async _getProfileInfo() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
const profileInfo = await cli.getProfileInfo(userId);
|
||||
const avatarUrl = Avatar.avatarUrlForUser(
|
||||
{avatarUrl: profileInfo.avatar_url},
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
|
||||
return {
|
||||
userId,
|
||||
name: profileInfo.displayname,
|
||||
avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const profileInfo = await this._getProfileInfo();
|
||||
this.setState({profileInfo});
|
||||
} catch (ex) {
|
||||
console.log("could not fetch profile");
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const fallbackUserId = MatrixClientPeg.get().getUserId();
|
||||
const profileInfo = this.state.profileInfo;
|
||||
const name = profileInfo ? profileInfo.name : fallbackUserId;
|
||||
let nameElement;
|
||||
if (!this.props.collapsed) {
|
||||
nameElement = <div className="mx_TopLeftMenuButton_name">
|
||||
{ name }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
|
||||
<BaseAvatar
|
||||
idName={fallbackUserId}
|
||||
name={name}
|
||||
url={profileInfo && profileInfo.avatarUrl}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
{ nameElement }
|
||||
<img className="mx_TopLeftMenuButton_chevron" src="img/topleft-chevron.svg" width="11" height="6" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
onToggleMenu(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top + elementRect.height;
|
||||
|
||||
ContextualMenu.createMenu(TopLeftMenu, {
|
||||
chevronFace: "none",
|
||||
left: x,
|
||||
top: y,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
}
|
|
@ -128,6 +128,7 @@ const CRYPTO_SETTINGS = [
|
|||
const THEMES = [
|
||||
{ label: _td('Light theme'), value: 'light' },
|
||||
{ label: _td('Dark theme'), value: 'dark' },
|
||||
{ label: _td('2018 theme'), value: 'dharma' },
|
||||
{ label: _td('Status.im theme'), value: 'status' },
|
||||
];
|
||||
|
||||
|
@ -383,32 +384,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
||||
title: _t("Sign out"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("For security, logging out will delete any end-to-end " +
|
||||
"encryption keys from this browser. If you want to be able " +
|
||||
"to decrypt your conversation history from future Riot sessions, " +
|
||||
"please export your room keys for safe-keeping.") }
|
||||
</div>,
|
||||
button: _t("Sign out"),
|
||||
extraButtons: [
|
||||
<button key="export" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t("Export E2E room keys") }
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const LogoutDialog = sdk.getComponent("dialogs.LogoutDialog");
|
||||
Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog);
|
||||
},
|
||||
|
||||
onPasswordChangeError: function(err) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue