Merge branches 'develop' and 't3chguy/emoji_picker_composer' of github.com:matrix-org/matrix-react-sdk into t3chguy/emoji_picker_composer

 Conflicts:
	src/components/views/rooms/MessageComposer.js
This commit is contained in:
Michael Telatynski 2020-05-29 14:53:42 +01:00
commit ccd0c952e3
1131 changed files with 52941 additions and 24634 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,93 +17,10 @@ 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.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar
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");
this.checkOverflow();
}
_collectContainerRef(ref) {
@ -126,9 +44,7 @@ export default class AutoHideScrollbar extends React.Component {
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
>
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children }
</div>
{ this.props.children }
</div>);
}
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,7 +21,7 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
module.exports = createReactClass({
export default createReactClass({
displayName: 'CompatibilityPage',
propTypes: {
onAccept: PropTypes.func,

View file

@ -21,7 +21,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Key} from "../../Keyboard";
import sdk from "../../index";
import * as sdk from "../../index";
import AccessibleButton from "../views/elements/AccessibleButton";
// Shamelessly ripped off Modal.js. There's probably a better way
@ -71,12 +71,12 @@ export class ContextMenu extends React.Component {
// on resize callback
windowResize: PropTypes.func,
catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true)
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
};
static defaultProps = {
hasBackground: true,
catchTab: true,
managed: true,
};
constructor() {
@ -186,15 +186,19 @@ export class ContextMenu extends React.Component {
};
_onKeyDown = (ev) => {
if (!this.props.managed) {
if (ev.key === Key.ESCAPE) {
this.props.onFinished();
ev.stopPropagation();
ev.preventDefault();
}
return;
}
let handled = true;
switch (ev.key) {
case Key.TAB:
if (!this.props.catchTab) {
handled = false;
break;
}
// fallthrough
case Key.ESCAPE:
this.props.onFinished();
break;
@ -241,7 +245,6 @@ export class ContextMenu extends React.Component {
}
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
const padding = 10;
const chevronOffset = {};
if (props.chevronFace) {
@ -251,7 +254,7 @@ export class ContextMenu extends React.Component {
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
} else if (position.top !== undefined) {
const target = position.top;
// By default, no adjustment is made
@ -260,7 +263,8 @@ export class ContextMenu extends React.Component {
// If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window.
if (contextMenuRect) {
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
const padding = 10;
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding);
}
position.top = adjusted;
@ -321,7 +325,7 @@ export class ContextMenu extends React.Component {
return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
{ chevron }
{ props.children }
</div>
@ -346,7 +350,7 @@ export const ContextMenuButton = ({ label, isExpanded, children, ...props }) =>
};
ContextMenuButton.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string.isRequired,
label: PropTypes.string,
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
};
@ -373,7 +377,6 @@ export const MenuGroup = ({children, label, ...props}) => {
</div>;
};
MenuGroup.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string.isRequired,
className: PropTypes.string, // optional
};

View file

@ -17,8 +17,8 @@ limitations under the License.
import React from 'react';
import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
import AutoHideScrollbar from './AutoHideScrollbar';
import sdk from '../../index';
import dis from '../../dispatcher';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils';
@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({tags: CustomRoomTagStore.getSortedTags()});
});
@ -61,30 +61,13 @@ class CustomRoomTagPanel extends React.Component {
}
class CustomRoomTagTile extends React.Component {
constructor(props) {
super(props);
this.state = {hover: false};
this.onClick = this.onClick.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
}
onMouseOver() {
this.setState({hover: true});
}
onMouseOut() {
this.setState({hover: false});
}
onClick() {
onClick = () => {
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
}
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Tooltip = sdk.getComponent('elements.Tooltip');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const tag = this.props.tag;
const avatarHeight = 40;
@ -102,12 +85,9 @@ class CustomRoomTagTile extends React.Component {
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
const tip = (this.state.hover ?
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />);
return (
<AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<AccessibleTooltipButton className={className} onClick={this.onClick} title={name}>
<div className="mx_TagTile_avatar">
<BaseAvatar
name={tag.avatarLetter}
idName={name}
@ -115,9 +95,8 @@ class CustomRoomTagTile extends React.Component {
height={avatarHeight}
/>
{ badgeElement }
{ tip }
</div>
</AccessibleButton>
</AccessibleTooltipButton>
);
}
}

View file

@ -23,11 +23,11 @@ import PropTypes from 'prop-types';
import request from 'browser-request';
import { _t } from '../../languageHandler';
import sanitizeHtml from 'sanitize-html';
import sdk from '../../index';
import dis from '../../dispatcher';
import MatrixClientPeg from '../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
export default class EmbeddedPage extends React.PureComponent {
static propTypes = {
@ -37,11 +37,11 @@ export default class EmbeddedPage extends React.PureComponent {
className: PropTypes.string,
// Whether to wrap the page in a scrollbar
scrollbar: PropTypes.bool,
// Map of keys to replace with values, e.g {$placeholder: "value"}
replaceMap: PropTypes.object,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static contextType = MatrixClientContext;
constructor(props) {
super(props);
@ -58,7 +58,7 @@ export default class EmbeddedPage extends React.PureComponent {
return sanitizeHtml(_t(s));
}
componentWillMount() {
componentDidMount() {
this._unmounted = false;
if (!this.props.url) {
@ -83,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent {
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach(key => {
body = body.split(key).join(this.props.replaceMap[key]);
});
}
this.setState({ page: body });
},
);
@ -104,7 +111,7 @@ export default class EmbeddedPage extends React.PureComponent {
render() {
// HACK: Workaround for the context's MatrixClient not updating.
const client = this.context.matrixClient || MatrixClientPeg.get();
const client = this.context || MatrixClientPeg.get();
const isGuest = client ? client.isGuest() : true;
const className = this.props.className;
const classes = classnames({
@ -119,10 +126,9 @@ export default class EmbeddedPage extends React.PureComponent {
</div>;
if (this.props.scrollbar) {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return <GeminiScrollbarWrapper autoshow={true} className={classes}>
return <AutoHideScrollbar className={classes}>
{content}
</GeminiScrollbarWrapper>;
</AutoHideScrollbar>;
} else {
return <div className={classes}>
{content}

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,9 +19,10 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import {Filter} from 'matrix-js-sdk';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
/*
@ -28,6 +30,9 @@ import { _t } from '../../languageHandler';
*/
const FilePanel = createReactClass({
displayName: 'FilePanel',
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
decryptingEvents: new Set(),
propTypes: {
roomId: PropTypes.string.isRequired,
@ -39,42 +44,147 @@ const FilePanel = createReactClass({
};
},
componentDidMount: function() {
this.updateTimelineSet(this.props.roomId);
onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
if (room.roomId !== this.props.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId());
} else {
this.addEncryptedLiveEvent(ev);
}
},
updateTimelineSet: function(roomId) {
onEventDecrypted(ev, err) {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId();
if (!this.decryptingEvents.delete(eventId)) return;
if (err) return;
this.addEncryptedLiveEvent(ev);
},
addEncryptedLiveEvent(ev, toStartOfTimeline) {
if (!this.state.timelineSet) return;
const timeline = this.state.timelineSet.getLiveTimeline();
if (ev.getType() !== "m.room.message") return;
if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) {
return;
}
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
}
},
async componentDidMount() {
const client = MatrixClientPeg.get();
await this.updateTimelineSet(this.props.roomId);
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
// The timelineSets filter makes sure that encrypted events that contain
// URLs never get added to the timeline, even if they are live events.
// These methods are here to manually listen for such events and add
// them despite the filter's best efforts.
//
// We do this only for encrypted rooms and if an event index exists,
// this could be made more general in the future or the filter logic
// could be fixed.
if (EventIndexPeg.get() !== null) {
client.on('Room.timeline', this.onRoomTimeline);
client.on('Event.decrypted', this.onEventDecrypted);
}
},
componentWillUnmount() {
const client = MatrixClientPeg.get();
if (client === null) return;
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
if (EventIndexPeg.get() !== null) {
client.removeListener('Room.timeline', this.onRoomTimeline);
client.removeListener('Event.decrypted', this.onEventDecrypted);
}
},
async fetchFileEventsServer(room) {
const client = MatrixClientPeg.get();
const filter = new Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true,
"types": [
"m.room.message",
],
},
},
},
);
const filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter);
filter.filterId = filterId;
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
return timelineSet;
},
onPaginationRequest(timelineWindow, direction, limit) {
const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
const room = client.getRoom(roomId);
// We override the pagination request for encrypted rooms so that we ask
// the event index to fulfill the pagination request. Asking the server
// to paginate won't ever work since the server can't correctly filter
// out events containing URLs
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit);
} else {
return timelineWindow.paginate(direction, limit);
}
},
async updateTimelineSet(roomId: string) {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
this.noRoom = !room;
if (room) {
const filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true,
"types": [
"m.room.message",
],
},
},
},
);
let timelineSet;
// FIXME: we shouldn't be doing this every time we change room - see comment above.
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{
filter.filterId = filterId;
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
this.setState({ timelineSet: timelineSet });
},
(error)=>{
console.error("Failed to get or create file panel filter", error);
},
);
try {
timelineSet = await this.fetchFileEventsServer(room);
// If this room is encrypted the file panel won't be populated
// correctly since the defined filter doesn't support encrypted
// events and the server can't check if encrypted events contain
// URLs.
//
// This is where our event index comes into place, we ask the
// event index to populate the timelineSet for us. This call
// will add 10 events to the live timeline of the set. More can
// be requested using pagination.
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
const timeline = timelineSet.getLiveTimeline();
await eventIndex.populateFileTimeline(timelineSet, timeline, room, 10);
}
this.setState({ timelineSet: timelineSet });
} catch (error) {
console.error("Failed to get or create file panel filter", error);
}
} else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
@ -110,6 +220,7 @@ const FilePanel = createReactClass({
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = {false}
onPaginationRequest={this.onPaginationRequest}
tileShape="file_grid"
resizeNotifier={this.props.resizeNotifier}
empty={_t('There are no visible files in this room')}
@ -126,4 +237,4 @@ const FilePanel = createReactClass({
},
});
module.exports = FilePanel;
export default FilePanel;

View file

@ -19,9 +19,9 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import { getHostingLink } from '../../utils/HostingLink';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td } from '../../languageHandler';
@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm
import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -91,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this summary?"),
placeholder: _t("Room name or alias"),
placeholder: _t("Room name or address"),
button: _t("Add to summary"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],
@ -423,28 +424,35 @@ export default createReactClass({
membershipBusy: false,
publicityBusy: false,
inviterProfile: null,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this._changeAvatarComponent = null;
this._initGroupStore(this.props.groupId, true);
this._dispatcherRef = dis.register(this._onAction);
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
},
componentWillUnmount: function() {
this._unmounted = true;
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
dis.unregister(this._dispatcherRef);
// Remove RightPanelStore listener
if (this._rightPanelStoreToken) {
this._rightPanelStoreToken.remove();
}
},
componentWillReceiveProps: function(newProps) {
if (this.props.groupId != newProps.groupId) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (this.props.groupId !== newProps.groupId) {
this.setState({
summary: null,
error: null,
@ -454,6 +462,12 @@ export default createReactClass({
}
},
_onRightPanelStoreUpdate: function() {
this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
});
},
_onGroupMyMembership: function(group) {
if (this._unmounted || group.groupId !== this.props.groupId) return;
if (group.myMembership === 'leave') {
@ -481,7 +495,7 @@ export default createReactClass({
group_id: groupId,
},
});
dis.dispatch({action: 'require_registration'});
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}});
willDoOnboarding = true;
}
if (stateKey === GroupStore.STATE_KEY.Summary) {
@ -554,10 +568,6 @@ export default createReactClass({
GROUP_JOINPOLICY_INVITE,
},
});
dis.dispatch({
action: 'panel_disable',
sideDisabled: true,
});
},
_onShareClick: function() {
@ -580,10 +590,6 @@ export default createReactClass({
profileForm: null,
});
break;
case 'after_right_panel_phase_change':
// We don't keep state on the right panel, so just re-render to update
this.forceUpdate();
break;
default:
break;
}
@ -726,7 +732,7 @@ export default createReactClass({
_onJoinClick: async function() {
if (this._matrixClient.isGuest()) {
dis.dispatch({action: 'require_registration'});
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
return;
}
@ -821,10 +827,10 @@ export default createReactClass({
{_t(
"Want more than a community? <a>Get your own server</a>", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
@ -1173,7 +1179,6 @@ export default createReactClass({
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Spinner = sdk.getComponent("elements.Spinner");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
return <Spinner />;
@ -1299,9 +1304,7 @@ export default createReactClass({
);
}
const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup
? <RightPanel groupId={this.props.groupId} />
: undefined;
const rightPanel = this.state.showRightPanel ? <RightPanel groupId={this.props.groupId} /> : undefined;
const headerClasses = {
"mx_GroupView_header": true,
@ -1332,10 +1335,10 @@ export default createReactClass({
<GroupHeaderButtons />
</div>
<MainSplit panel={rightPanel}>
<GeminiScrollbarWrapper className="mx_GroupView_body">
<AutoHideScrollbar className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }
</GeminiScrollbarWrapper>
</AutoHideScrollbar>
</MainSplit>
</main>
);

View file

@ -0,0 +1,66 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 * as React from "react";
import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages";
import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
const HomePage = () => {
const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config);
if (pageUrl) {
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
}
const brandingConfig = config.branding;
let logoUrl = "themes/riot/img/logos/riot-logo.svg";
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
logoUrl = brandingConfig.authHeaderLogoUrl;
}
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
<img src={logoUrl} alt="Riot" />
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
<div className="mx_HomePage_default_buttons">
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
{ _t("Send a Direct Message") }
</AccessibleButton>
<AccessibleButton onClick={onClickExplore} className="mx_HomePage_button_explore">
{ _t("Explore Public Rooms") }
</AccessibleButton>
<AccessibleButton onClick={onClickNewRoom} className="mx_HomePage_button_createGroup">
{ _t("Create a Group Chat") }
</AccessibleButton>
</div>
</div>
</AutoHideScrollbar>;
};
export default HomePage;

View file

@ -66,6 +66,22 @@ export default class IndicatorScrollbar extends React.Component {
this._autoHideScrollbar = autoHideScrollbar;
}
componentDidUpdate(prevProps) {
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
const curLen = this.props.children && this.props.children.length || 0;
// check overflow only if amount of children changes.
// if we don't guard here, we end up with an infinite
// render > componentDidUpdate > checkOverflow > setState > render loop
if (prevLen !== curLen) {
this.checkOverflow();
}
}
componentDidMount() {
this.checkOverflow();
}
checkOverflow() {
const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scrollElement.scrollHeight >
@ -95,10 +111,6 @@ export default class IndicatorScrollbar extends React.Component {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
}
if (this._autoHideScrollbar) {
this._autoHideScrollbar.checkOverflow();
}
if (this.props.trackHorizontalOverflow) {
this.setState({
// Offset from absolute position of the container

View file

@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,16 +15,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import {InteractiveAuth} from "matrix-js-sdk";
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents';
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
import sdk from '../../index';
import * as sdk from '../../index';
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
export default createReactClass({
displayName: 'InteractiveAuth',
@ -49,7 +49,7 @@ export default createReactClass({
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param {object} result The result of the authenticated call
// if successful, otherwise the error object
// if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
@ -77,6 +77,15 @@ export default createReactClass({
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
// Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric).
onStagePhaseChange: PropTypes.func,
// continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText: PropTypes.string,
continueKind: PropTypes.string,
},
getInitialState: function() {
@ -89,7 +98,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
@ -163,6 +173,7 @@ export default createReactClass({
_authStateUpdated: function(stageType, stageState) {
const oldStage = this.state.authStage;
this.setState({
busy: false,
authStage: stageType,
stageState: stageState,
errorText: stageState.error,
@ -186,11 +197,13 @@ export default createReactClass({
errorText: null,
stageErrorText: null,
});
} else {
this.setState({
busy: false,
});
}
// The JS SDK eagerly reports itself as "not busy" right after any
// immediate work has completed, but that's not really what we want at
// the UI layer, so we ignore this signal and show a spinner until
// there's a new screen to show the user. This is implemented by setting
// `busy: false` in `_authStateUpdated`.
// See also https://github.com/vector-im/riot-web/issues/12546
},
_setFocus: function() {
@ -203,6 +216,16 @@ export default createReactClass({
this._authLogic.submitAuthDict(authData);
},
_onPhaseChange: function(newPhase) {
if (this.props.onStagePhaseChange) {
this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
}
},
_onStageCancel: function() {
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
},
_renderCurrentStage: function() {
const stage = this.state.authStage;
if (!stage) {
@ -231,6 +254,10 @@ export default createReactClass({
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
showContinue={!this.props.continueIsManaged}
onPhaseChange={this._onPhaseChange}
continueText={this.props.continueText}
continueKind={this.props.continueKind}
onCancel={this._onStageCancel}
/>
);
},

View file

@ -19,15 +19,14 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import { Key } from '../../Keyboard';
import sdk from '../../index';
import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
const LeftPanel = createReactClass({
@ -39,10 +38,6 @@ const LeftPanel = createReactClass({
collapsed: PropTypes.bool.isRequired,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState: function() {
return {
searchFilter: '',
@ -50,7 +45,8 @@ const LeftPanel = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this.focusedElement = null;
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
@ -135,9 +131,6 @@ const LeftPanel = createReactClass({
if (!this.focusedElement) return;
switch (ev.key) {
case Key.TAB:
this._onMoveFocus(ev, ev.shiftKey);
break;
case Key.ARROW_UP:
this._onMoveFocus(ev, true, true);
break;
@ -243,7 +236,6 @@ const LeftPanel = createReactClass({
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
<TagPanelButtons />
</div>);
}
@ -282,6 +274,29 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
let roomList = null;
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
roomList = <RoomList2
onKeyDown={this._onKeyDown}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ref={this.collectRoomList}
onFocus={this._onFocus}
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
return (
<div className={containerClasses}>
{ tagPanelContainer }
@ -293,19 +308,11 @@ const LeftPanel = createReactClass({
{ exploreButton }
{ searchBox }
</div>
<RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />
{roomList}
</aside>
</div>
);
},
});
module.exports = LeftPanel;
export default LeftPanel;

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2017, 2018, 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,28 +16,42 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from 'matrix-js-sdk';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd';
import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import sdk from '../../index';
import dis from '../../dispatcher';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import { getHomePageUrl } from '../../utils/pages';
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
hideToast as hideSetPasswordToast
} from "../../toasts/SetPasswordToast";
import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
// 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.
@ -50,6 +64,44 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable");
}
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
useCompactLayout: boolean;
}
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property.
@ -59,10 +111,10 @@ function canElementReceiveInput(el) {
*
* Components mounted below us can access the matrix client via the react context.
*/
const LoggedInView = createReactClass({
displayName: 'LoggedInView',
class LoggedInView extends React.PureComponent<IProps, IState> {
static displayName = 'LoggedInView';
propTypes: {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
@ -75,39 +127,25 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff.
},
};
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
authCache: PropTypes.object,
},
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected resizer: Resizer;
getChildContext: function() {
return {
matrixClient: this._matrixClient,
authCache: {
auth: {},
lastUpdate: 0,
},
};
},
constructor(props, context) {
super(props, context);
getInitialState: function() {
return {
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
};
},
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;
@ -129,22 +167,26 @@ const LoggedInView = createReactClass({
fixupColorFonts();
this._roomView = createRef();
},
this._roomView = React.createRef();
this._resizeContainer = React.createRef();
}
componentDidUpdate(prevProps) {
componentDidMount() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed
if (
(prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
(prevProps.checkingForUpdate !== this.props.checkingForUpdate)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
@ -153,7 +195,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove();
}
this.resizer.detach();
},
}
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
@ -161,22 +203,24 @@ const LoggedInView = createReactClass({
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() {
shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get());
},
}
canResetTimelineInRoom: function(roomId) {
canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) {
return true;
}
return this._roomView.current.canResetTimeline();
},
};
_setStateFromSessionStore() {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
_setStateFromSessionStore = () => {
if (this._sessionStore.getCachedPassword()) {
showSetPasswordToast();
} else {
hideSetPasswordToast();
}
};
_createResizer() {
const classNames = {
@ -200,24 +244,22 @@ const LoggedInView = createReactClass({
},
};
const resizer = new Resizer(
this.resizeContainer,
this._resizeContainer.current,
CollapseDistributor,
collapseConfig);
resizer.setClassNames(classNames);
return resizer;
},
}
_loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size");
if (lhsSize !== null) {
lhsSize = parseInt(lhsSize, 10);
} else {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (isNaN(lhsSize)) {
lhsSize = 350;
}
this.resizer.forHandleAt(0).resize(lhsSize);
},
}
onAccountData: function(event) {
onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
@ -226,9 +268,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
},
};
onSync: function(syncState, oldSyncState, data) {
onSync = (syncState, oldSyncState, data) => {
const oldErrCode = (
this.state.syncErrorData &&
this.state.syncErrorData.error &&
@ -249,22 +291,37 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(data);
}
},
};
onRoomStateEvents: function(ev, state) {
const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
},
};
_updateServerNoticeEvents: async function() {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
const pinnedEvents = [];
for (const room of roomLists['m.server_notice']) {
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
} else {
hideServerLimitToast();
}
}
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return [];
const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -272,16 +329,22 @@ const LoggedInView = createReactClass({
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) events.push(event);
}
}
this.setState({
serverNoticeEvents: pinnedEvents,
});
},
_onPaste: function(ev) {
const usageLimitEvent = events.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
};
_onPaste = (ev) => {
let canReceiveInput = false;
let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element
@ -295,7 +358,7 @@ const LoggedInView = createReactClass({
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
}
},
};
/*
SOME HACKERY BELOW:
@ -319,22 +382,22 @@ const LoggedInView = createReactClass({
We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element.
*/
_onReactKeyDown: function(ev) {
_onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element
// of this component, so something must be focused.
this._onKeyDown(ev);
},
};
_onNativeKeyDown: function(ev) {
_onNativeKeyDown = (ev) => {
// only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order.
if (ev.target === document.body) {
this._onKeyDown(ev);
}
},
};
_onKeyDown: function(ev) {
_onKeyDown = (ev) => {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this.
@ -351,13 +414,13 @@ const LoggedInView = createReactClass({
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey ||
ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
switch (ev.key) {
case Key.PAGE_UP:
case Key.PAGE_DOWN:
if (!hasModifier) {
if (!hasModifier && !isModifier) {
this._onScrollKeyPressed(ev);
handled = true;
}
@ -379,8 +442,6 @@ const LoggedInView = createReactClass({
}
break;
case Key.BACKTICK:
if (ev.key !== "`") break;
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"
// was previously chosen but conflicted with italics in
@ -393,12 +454,48 @@ const LoggedInView = createReactClass({
handled = true;
}
break;
case Key.SLASH:
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
KeyboardShortcuts.toggleDialog();
handled = true;
}
break;
case Key.ARROW_UP:
case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
dis.dispatch({
action: 'view_room_delta',
delta: ev.key === Key.ARROW_UP ? -1 : 1,
unread: ev.shiftKey,
});
handled = true;
}
break;
case Key.PERIOD:
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
dis.dispatch({
action: 'toggle_right_panel',
type: this.props.page_type === "room_view" ? "room" : "group",
});
handled = true;
}
break;
default:
// if we do not have a handler for it, pass it to the platform which might
handled = PlatformPeg.get().onKeyDown(ev);
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
} else if (!hasModifier) {
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// The above condition is crafted to _allow_ characters with Shift
// already pressed (but not the Shift key down itself).
const isClickShortcut = ev.target !== document.body &&
(ev.key === Key.SPACE || ev.key === Key.ENTER);
@ -407,13 +504,6 @@ const LoggedInView = createReactClass({
return;
}
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
// If using Slate, consume the Backspace without first focusing as it causes an implosion
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
ev.stopPropagation();
return;
}
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true);
@ -422,19 +512,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer
}
}
},
};
/**
* dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event
*/
_onScrollKeyPressed: function(ev) {
_onScrollKeyPressed = (ev) => {
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
},
};
_onDragEnd: function(result) {
_onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
@ -457,9 +547,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result);
}
},
};
_onRoomTileEndDrag: function(result) {
_onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined;
@ -476,9 +566,9 @@ const LoggedInView = createReactClass({
prevTag, newTag,
oldIndex, newIndex,
), true);
},
};
_onMouseDown: function(ev) {
_onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group).
@ -497,9 +587,9 @@ const LoggedInView = createReactClass({
});
}
}
},
};
_onMouseUp: function(ev) {
_onMouseUp = (ev) => {
if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x;
@ -518,26 +608,16 @@ const LoggedInView = createReactClass({
// Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks.
this.setState({mouseDown: null});
},
};
_setResizeContainerRef(div) {
this.resizeContainer = div;
},
render: function() {
render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
let pageElement;
@ -567,13 +647,7 @@ const LoggedInView = createReactClass({
break;
case PageTypes.HomePage:
{
const pageUrl = getHomePageUrl(this.props.config);
pageElement = <EmbeddedPage className="mx_HomePage"
url={pageUrl}
scrollbar={true}
/>;
}
pageElement = <HomePage />;
break;
case PageTypes.UserView:
@ -587,39 +661,9 @@ const LoggedInView = createReactClass({
break;
}
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
let topBar;
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
topBar = <ServerLimitBar kind='hard'
adminContact={this.state.syncErrorData.error.data.admin_contact}
limitType={this.state.syncErrorData.error.data.limit_type}
/>;
} else if (usageLimitEvent) {
topBar = <ServerLimitBar kind='soft'
adminContact={usageLimitEvent.getContent().admin_contact}
limitType={usageLimitEvent.getContent().limit_type}
/>;
} else if (this.props.showCookieBar &&
this.props.config.piwik
) {
const policyUrl = this.props.config.piwik.policyUrl || null;
topBar = <CookieBar policyUrl={policyUrl} />;
} else if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (this.props.showNotifierToolbar) {
topBar = <MatrixToolbar />;
}
let bodyClasses = 'mx_MatrixChat';
@ -631,23 +675,32 @@ const LoggedInView = createReactClass({
}
return (
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle />
{ pageElement }
</div>
</DragDropContext>
</div>
<MatrixClientContext.Provider value={this._matrixClient}>
<div
onPaste={this._onPaste}
onKeyDown={this._onReactKeyDown}
className='mx_MatrixChat_wrapper'
aria-hidden={this.props.hideToSRUsers}
onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp}
>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle />
{ pageElement }
</div>
</DragDropContext>
</div>
</MatrixClientContext.Provider>
);
},
});
}
}
export default LoggedInView;

View file

@ -74,18 +74,38 @@ export default class MainSplit extends React.Component {
}
}
componentDidUpdate(prevProps) {
const wasPanelSet = this.props.panel && !prevProps.panel;
const wasPanelCleared = !this.props.panel && prevProps.panel;
if (this.resizeContainer && wasPanelSet) {
// The resizer can only be created when **both** expanded and the panel is
// set. Once both are true, the container ref will mount, which is required
// for the resizer to work.
this._createResizer();
} else if (this.resizer && 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 }
const hasResizer = !this.props.collapsedRhs && panelView;
let children;
if (hasResizer) {
children = <React.Fragment>
<ResizeHandle reverse={true} />
{ panelView }
</div>;
</React.Fragment>;
}
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
{ bodyView }
{ children }
</div>;
}
}

View file

@ -22,11 +22,14 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import sdk from '../../index';
import * as sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@ -107,13 +110,16 @@ export default class MessagePanel extends React.Component {
showReactions: PropTypes.bool,
};
constructor() {
super();
// Force props to be loaded for useIRCLayout
constructor(props) {
super(props);
this.state = {
// previous positions the read marker has been in, so we can
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -163,6 +169,11 @@ export default class MessagePanel extends React.Component {
this._readMarkerNode = createRef();
this._whoIsTyping = createRef();
this._scrollPanel = createRef();
this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
}
componentDidMount() {
@ -171,6 +182,8 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
SettingsStore.unwatchSetting(this._layoutWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -183,6 +196,23 @@ export default class MessagePanel extends React.Component {
}
}
onShowTypingNotificationsChange = () => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
onLayoutChange = () => {
this.setState({
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
});
}
useIRCLayout(ircLayoutSelected) {
// if room is null we are not in a normal room list
return ircLayoutSelected && this.props.room;
}
/* get the DOM node representing the given event */
getNodeForEventId(eventId) {
if (!this.eventNodes) {
@ -318,8 +348,7 @@ export default class MessagePanel extends React.Component {
return true;
}
const EventTile = sdk.getComponent('rooms.EventTile');
if (!EventTile.haveTileForEvent(mxEv)) {
if (!haveTileForEvent(mxEv)) {
return false; // no tile = no show
}
@ -402,10 +431,6 @@ export default class MessagePanel extends React.Component {
};
_getEventTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
let i;
@ -447,190 +472,55 @@ export default class MessagePanel extends React.Component {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
}
let grouper = null;
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
const shouldGroup = (ev) => {
if (ev.getType() === "m.room.member"
&& (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
if (ev.isState() && ev.getSender() === mxEv.getSender()) {
return true;
}
return false;
};
if (mxEv.getType() === "m.room.create") {
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator);
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv);
continue;
} else {
// not part of group, so get the group tiles, close the
// group, and continue like a normal event
ret.push(...grouper.getTiles());
prevEvent = grouper.getNewPrevEvent();
grouper = null;
}
// If RM event is the first in the summary, append the RM after the summary
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (this._shouldShowEvent(mxEv)) {
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
}
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
break;
}
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
}
// At this point, i = the index of the last event in the summary sequence
const eventTiles = summarisedEvents.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return this._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.props.events[i];
ret.push(<EventListSummary
key="roomcreationsummary"
events={summarisedEvents}
onToggle={this._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={_t("%(creator)s created and configured the room.", {
creator: ev.sender ? ev.sender.name : ev.getSender(),
})}
>
{ eventTiles }
</EventListSummary>);
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
continue;
}
const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) {
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
// method on MELS can be used to prevent unnecessary renderings.
//
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
// membership event, which will not change during forward pagination.
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator);
for (const Grouper of groupers) {
if (Grouper.canStartGroup(this, mxEv)) {
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
}
// If RM event is the first in the MELS, append the RM after MELS
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
if (!isMembershipChange(collapsedMxEv) ||
this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
break;
}
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
}
let highlightInMels = false;
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map((e) => {
if (e.getId() === this.props.highlightedEventId) {
highlightInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return this._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(<MemberEventListSummary key={key}
events={summarisedEvents}
onToggle={this._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
>
{ eventTiles }
</MemberEventListSummary>);
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
continue;
}
if (!grouper) {
const wantTile = this._shouldShowEvent(mxEv);
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
}
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
}
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
if (grouper) {
ret.push(...grouper.getTiles());
}
return ret;
}
_getTilesForEvent(prevEvent, mxEv, last) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = [];
@ -651,7 +541,8 @@ export default class MessagePanel extends React.Component {
// if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
continuation = true;
}
@ -704,25 +595,28 @@ export default class MessagePanel extends React.Component {
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
<EventTile mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting.bind(this)}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
/>
<TileErrorBoundary mxEvent={mxEv}>
<EventTile mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting.bind(this)}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
useIRCLayout={this.state.useIRCLayout}
/>
</TileErrorBoundary>
</li>,
);
@ -884,6 +778,7 @@ export default class MessagePanel extends React.Component {
}
render() {
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
const Spinner = sdk.getComponent("elements.Spinner");
@ -902,11 +797,13 @@ export default class MessagePanel extends React.Component {
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
"mx_IRCLayout": this.state.useIRCLayout,
"mx_GroupLayout": !this.state.useIRCLayout,
},
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape) {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}
@ -915,23 +812,285 @@ export default class MessagePanel extends React.Component {
);
}
let ircResizer = null;
if (this.state.useIRCLayout) {
ircResizer = <IRCTimelineProfileResizer
minWidth={20}
maxWidth={600}
roomId={this.props.room ? this.props.roomroomId : null}
/>;
}
return (
<ScrollPanel
ref={this._scrollPanel}
className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}
>
{ topSpinner }
{ this._getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
</ScrollPanel>
<ErrorBoundary>
<ScrollPanel
ref={this._scrollPanel}
className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}
fixedChildren={ircResizer}
>
{ topSpinner }
{ this._getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
</ScrollPanel>
</ErrorBoundary>
);
}
}
/* Grouper classes determine when events can be grouped together in a summary.
* Groupers should have the following methods:
* - canStartGroup (static): determines if a new group should be started with the
* given event
* - shouldGroup: determines if the given event should be added to an existing group
* - add: adds an event to an existing group (should only be called if shouldGroup
* return true)
* - getTiles: returns the tiles that represent the group
* - getNewPrevEvent: returns the event that should be used as the new prevEvent
* when determining things such as whether a date separator is necessary
*/
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
class CreationGrouper {
static canStartGroup = function(panel, ev) {
return ev.getType() === "m.room.create";
};
constructor(panel, createEvent, prevEvent, lastShownEvent) {
this.panel = panel;
this.createEvent = createEvent;
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
this.events = [];
// events that we include in the group but then eject out and place
// above the group.
this.ejectedEvents = [];
this.readMarker = panel._readMarkerForEvent(
createEvent.getId(),
createEvent === lastShownEvent,
);
}
shouldGroup(ev) {
const panel = this.panel;
const createEvent = this.createEvent;
if (!panel._shouldShowEvent(ev)) {
return true;
}
if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
return false;
}
if (ev.getType() === "m.room.member"
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
if (ev.isState() && ev.getSender() === createEvent.getSender()) {
return true;
}
return false;
}
add(ev) {
const panel = this.panel;
this.readMarker = this.readMarker || panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
if (!panel._shouldShowEvent(ev)) {
return;
}
if (ev.getType() === "m.room.encryption") {
this.ejectedEvents.push(ev);
} else {
this.events.push(ev);
}
}
getTiles() {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const createEvent = this.createEvent;
const lastShownEvent = this.lastShownEvent;
if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (panel._shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
}
for (const ejected of this.ejectedEvents) {
ret.push(...panel._getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent,
));
}
const eventTiles = this.events.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
ret.push(
<EventListSummary
key="roomcreationsummary"
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={_t("%(creator)s created and configured the room.", {
creator: ev.sender ? ev.sender.name : ev.getSender(),
})}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.createEvent;
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper {
static canStartGroup = function(panel, ev) {
return panel._shouldShowEvent(ev) && isMembershipChange(ev);
}
constructor(panel, ev, prevEvent, lastShownEvent) {
this.panel = panel;
this.readMarker = panel._readMarkerForEvent(
ev.getId(),
ev === lastShownEvent,
);
this.events = [ev];
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
}
shouldGroup(ev) {
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return isMembershipChange(ev);
}
add(ev) {
if (ev.getType() === 'm.room.member') {
// We'll just double check that it's worth our time to do so, through an
// ugly hack. If textForEvent returns something, we should group it for
// rendering but if it doesn't then we'll exclude it.
const renderText = textForEvent(ev);
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
}
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev);
}
getTiles() {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
// method on MELS can be used to prevent unnecessary renderings.
//
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
// membership event, which will not change during forward pagination.
const key = "membereventlistsummary-" + (
this.prevEvent ? this.events[0].getId() : "initial"
);
let highlightInMels;
let eventTiles = this.events.map((e) => {
if (e.getId() === panel.props.highlightedEventId) {
highlightInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<MemberEventListSummary key={key}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
>
{ eventTiles }
</MemberEventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.events[0];
}
}
// all the grouper classes that we use
const groupers = [CreationGrouper, MemberGrouper];

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../index';
import * as sdk from '../../index';
import { _t } from '../../languageHandler';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
export default createReactClass({
displayName: 'MyGroups',
@ -34,11 +34,11 @@ export default createReactClass({
};
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
statics: {
contextType: MatrixClientContext,
},
componentWillMount: function() {
componentDidMount: function() {
this._fetch();
},
@ -47,7 +47,7 @@ export default createReactClass({
},
_fetch: function() {
this.context.matrixClient.getJoinedGroups().then((result) => {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
@ -63,8 +63,6 @@ export default createReactClass({
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const GroupTile = sdk.getComponent("groups.GroupTile");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
let content;
let contentHeader;
@ -75,7 +73,7 @@ export default createReactClass({
});
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
content = groupNodes.length > 0 ?
<GeminiScrollbarWrapper>
<AutoHideScrollbar className="mx_MyGroups_scrollable">
<div className="mx_MyGroups_microcopy">
<p>
{ _t(
@ -94,7 +92,7 @@ export default createReactClass({
<div className="mx_MyGroups_joinedGroups">
{ groupNodes }
</div>
</GeminiScrollbarWrapper> :
</AutoHideScrollbar> :
<div className="mx_MyGroups_placeholder">
{ _t(
"You're not currently a member of any communities.",

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,8 +19,8 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../languageHandler';
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
/*
* Component which shows the global notification list using a TimelinePanel
@ -60,4 +61,4 @@ const NotificationPanel = createReactClass({
},
});
module.exports = NotificationPanel;
export default NotificationPanel;

View file

@ -21,15 +21,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
export default class RightPanel extends React.Component {
static get propTypes() {
@ -40,18 +41,15 @@ export default class RightPanel extends React.Component {
};
}
static get contextTypes() {
return {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
}
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
phase: this._getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest,
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
@ -72,15 +70,35 @@ export default class RightPanel extends React.Component {
return this.props.user || lastParams['member'];
}
// gets the current phase from the props and also maybe the store
_getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
const userForPanel = this._getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
return RIGHT_PANEL_PHASES.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (this._getUserForPanel()) {
} else if (userForPanel) {
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
// from its props and some from a store, except if the contents of the store changes
// while it's mounted in which case it replaces all of its state with that of the store,
// except it uses a dispatch instead of a normal store listener?
// Unfortunately rewriting this would almost certainly break showing the right panel
// in some of the many cases, and I don't have time to re-architect it and test all
// the flows now, so adding yet another special case so if the store thinks there is
// a verification going on for the member we're displaying, we show that, otherwise
// we race if a verification is started while the panel isn't displayed because we're
// not mounted in time to get the dispatch.
// Until then, let this code serve as a warning from history.
if (
rps.roomPanelPhaseParams.member &&
userForPanel.userId === rps.roomPanelPhaseParams.member.userId &&
rps.roomPanelPhaseParams.verificationRequest
) {
return rps.roomPanelPhase;
}
return RIGHT_PANEL_PHASES.RoomMemberInfo;
} else {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
@ -91,22 +109,23 @@ export default class RightPanel extends React.Component {
}
}
componentWillMount() {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context.matrixClient;
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
if (this.context.matrixClient) {
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore(this.props.groupId);
}
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
@ -164,6 +183,8 @@ export default class RightPanel extends React.Component {
groupId: payload.groupId,
member: payload.member,
event: payload.event,
verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise,
});
}
}
@ -183,62 +204,100 @@ export default class RightPanel extends React.Component {
let panel = <div />;
if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
member: null,
});
};
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
key={this.props.roomId || this.state.member.userId}
onClose={onClose}
/>;
} else {
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
}
} else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
member: null,
});
};
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
onClose={onClose} />;
} else {
panel = (
<GroupMemberInfo
groupMember={this.state.member}
switch (this.state.phase) {
case RIGHT_PANEL_PHASES.RoomMemberList:
if (this.props.roomId) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupMemberList:
if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
if (this.props.user) {
// If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviously the correct thing to do, but I'm not sure
// anything else is - we could hide the close button altogether?)
dis.dispatch({
action: "view_home_page",
});
} else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null,
});
}
};
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
key={this.props.roomId || this.state.member.userId}
onClose={onClose}
phase={this.state.phase}
verificationRequest={this.state.verificationRequest}
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
} else {
panel = <MemberInfo
member={this.state.member}
key={this.props.roomId || this.state.member.userId}
/>;
}
break;
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break;
case RIGHT_PANEL_PHASES.GroupMemberInfo:
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: Action.ViewUser,
member: null,
});
};
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.user_id}
/>
);
}
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) {
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
panel = <NotificationPanel />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
key={this.state.member.userId}
onClose={onClose} />;
} else {
panel = (
<GroupMemberInfo
groupMember={this.state.member}
groupId={this.props.groupId}
key={this.state.member.user_id}
/>
);
}
break;
case RIGHT_PANEL_PHASES.GroupRoomInfo:
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RIGHT_PANEL_PHASES.NotificationPanel:
panel = <NotificationPanel />;
break;
case RIGHT_PANEL_PHASES.FilePanel:
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
break;
}
const classes = classNames("mx_RightPanel", "mx_fadable", {
@ -248,7 +307,7 @@ export default class RightPanel extends React.Component {
});
return (
<aside className={classes}>
<aside className={classes} id="mx_RightPanel">
{ panel }
</aside>
);

View file

@ -18,18 +18,17 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
const MatrixClientPeg = require('../../MatrixClientPeg');
const ContentRepo = require("matrix-js-sdk").ContentRepo;
const Modal = require('../../Modal');
const sdk = require('../../index');
const dis = require('../../dispatcher');
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;
@ -38,44 +37,27 @@ function track(action) {
Analytics.trackEvent('RoomDirectory', action);
}
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomDirectory',
propTypes: {
config: PropTypes.object,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
config: {},
};
},
getInitialState: function() {
return {
publicRooms: [],
loading: true,
protocolsLoading: true,
error: null,
instanceId: null,
includeAll: false,
roomServer: null,
instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(),
filterString: null,
};
},
childContextTypes: {
matrixClient: PropTypes.object,
},
getChildContext: function() {
return {
matrixClient: MatrixClientPeg.get(),
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this.nextBatch = null;
this.filterTimeout = null;
@ -108,6 +90,8 @@ module.exports = createReactClass({
),
});
});
this.refreshRoomList();
},
componentWillUnmount: function() {
@ -142,10 +126,10 @@ module.exports = createReactClass({
if (my_server != MatrixClientPeg.getHomeserverName()) {
opts.server = my_server;
}
if (this.state.instanceId) {
opts.third_party_instance_id = this.state.instanceId;
} else if (this.state.includeAll) {
if (this.state.instanceId === ALL_ROOMS) {
opts.include_all_networks = true;
} else if (this.state.instanceId) {
opts.third_party_instance_id = this.state.instanceId;
}
if (this.nextBatch) opts.since = this.nextBatch;
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
@ -167,7 +151,7 @@ module.exports = createReactClass({
this.nextBatch = data.next_batch;
this.setState((s) => {
s.publicRooms.push(...data.chunk);
s.publicRooms.push(...(data.chunk || []));
s.loading = false;
return s;
});
@ -215,7 +199,7 @@ module.exports = createReactClass({
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
} else {
desc = _t('Remove %(name)s from the directory?', {name: name});
}
@ -232,7 +216,7 @@ module.exports = createReactClass({
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
step = _t('delete the alias.');
step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias);
}).then(() => {
modal.close();
@ -259,7 +243,7 @@ module.exports = createReactClass({
}
},
onOptionChange: function(server, instanceId, includeAll) {
onOptionChange: function(server, instanceId) {
// clear next batch so we don't try to load more rooms
this.nextBatch = null;
this.setState({
@ -269,7 +253,6 @@ module.exports = createReactClass({
publicRooms: [],
roomServer: server,
instanceId: instanceId,
includeAll: includeAll,
error: null,
}, this.refreshRoomList);
// We also refresh the room list each time even though this
@ -317,7 +300,7 @@ module.exports = createReactClass({
onJoinFromSearchClick: function(alias) {
// If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId) {
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
// If the user specified an alias without a domain, add on whichever server is selected
// in the dropdown
if (alias.indexOf(':') == -1) {
@ -384,7 +367,10 @@ module.exports = createReactClass({
onCreateRoomClick: function(room) {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
dis.dispatch({
action: 'view_create_room',
public: true,
});
},
showRoomAlias: function(alias, autoJoin=false) {
@ -418,6 +404,12 @@ module.exports = createReactClass({
// would normally decide what the name is.
name: room.name || room_alias || _t('Unnamed room'),
};
if (this.state.roomServer) {
payload.opts = {
viaServers: [this.state.roomServer],
};
}
}
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
// which servers to start querying. However, there's no other way to join rooms in
@ -466,7 +458,7 @@ module.exports = createReactClass({
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
const avatarUrl = ContentRepo.getHttpUriForMxc(
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatar_url, 32, 32, "crop",
);
@ -599,7 +591,7 @@ module.exports = createReactClass({
}
let placeholder = _t('Find a room…');
if (!this.state.instanceId) {
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
} else if (instance_expected_field_type) {
placeholder = instance_expected_field_type.placeholder;
@ -616,10 +608,18 @@ module.exports = createReactClass({
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder} showJoinButton={showJoinButton}
onChange={this.onFilterChange}
onClear={this.onFilterClear}
onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder}
showJoinButton={showJoinButton}
/>
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
</div>;
}
const explanation =
@ -640,7 +640,7 @@ module.exports = createReactClass({
title={_t("Explore rooms")}
>
<div className="mx_RoomDirectory">
<p>{explanation}</p>
{explanation}
<div className="mx_RoomDirectory_list">
{listHeader}
{content}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,12 +21,12 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -38,7 +39,7 @@ function getUnsentMessages(room) {
});
}
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomStatusBar',
propTypes: {
@ -95,7 +96,7 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
@ -219,12 +220,12 @@ module.exports = createReactClass({
});
if (hasUDE) {
title = _t("Message not sent due to unknown devices being present");
title = _t("Message not sent due to unknown sessions being present");
content = _t(
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
{},
{
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
@ -272,7 +273,7 @@ module.exports = createReactClass({
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = unsentMessages[0].error.data.error;
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else {
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
}

View file

@ -19,9 +19,9 @@ limitations under the License.
import React, {createRef} from 'react';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
import Unread from '../../Unread';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils';
import IndicatorScrollbar from './IndicatorScrollbar';
@ -31,10 +31,48 @@ import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore
const debug = false;
class RoomTileErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
console.error(
"The above error occured while React was rendering the following components:",
componentStack,
);
}
render() {
if (this.state.error) {
return (<div className="mx_RoomTile mx_RoomTileError">
{this.props.roomId}
</div>);
} else {
return this.props.children;
}
}
}
export default class RoomSubList extends React.PureComponent {
static displayName = 'RoomSubList';
static debug = debug;
@ -45,8 +83,6 @@ export default class RoomSubList extends React.PureComponent {
tagName: PropTypes.string,
addRoomLabel: PropTypes.string,
order: PropTypes.string.isRequired,
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
isInvite: PropTypes.bool,
@ -112,21 +148,30 @@ export default class RoomSubList extends React.PureComponent {
}
onAction = (payload) => {
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
//
// Ultimately we need to transition to a state pushing flow where something
// explicitly notifies the components concerned that the notif count for a room
// has change (e.g. a Flux store).
if (payload.action === 'on_room_read' &&
this.props.list.some((r) => r.roomId === payload.roomId)
) {
this.forceUpdate();
switch (payload.action) {
case 'on_room_read':
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
//
// Ultimately we need to transition to a state pushing flow where something
// explicitly notifies the components concerned that the notif count for a room
// has change (e.g. a Flux store).
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
this.forceUpdate();
}
break;
case 'view_room':
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
this.props.list.some((r) => r.roomId === payload.room_id)
) {
this.toggle();
}
}
};
onClick = (ev) => {
toggle = () => {
if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
@ -139,12 +184,12 @@ export default class RoomSubList extends React.PureComponent {
}
};
onClick = (ev) => {
this.toggle();
};
onHeaderKeyDown = (ev) => {
switch (ev.key) {
case Key.TAB:
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
ev.stopPropagation();
break;
case Key.ARROW_LEFT:
// On ARROW_LEFT collapse the room sublist
if (!this.state.hidden && !this.props.forceExpand) {
@ -185,6 +230,7 @@ export default class RoomSubList extends React.PureComponent {
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
show_room_tile: true, // to make sure the room gets scrolled into view
room_id: roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
});
@ -198,7 +244,7 @@ export default class RoomSubList extends React.PureComponent {
};
makeRoomTile = (room) => {
return <RoomTile
return <RoomTileErrorBoundary roomId={room.roomId}><RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
@ -211,7 +257,7 @@ export default class RoomSubList extends React.PureComponent {
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/>;
/></RoomTileErrorBoundary>;
};
_onNotifBadgeClick = (e) => {
@ -263,33 +309,6 @@ export default class RoomSubList extends React.PureComponent {
const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
// When collapsed, allow a long hover on the header to show user
// the full tag name and room count
let title;
@ -305,17 +324,6 @@ export default class RoomSubList extends React.PureComponent {
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
const len = this.props.list.length + this.props.extraTiles.length;
let chevron;
if (len) {
@ -327,25 +335,81 @@ export default class RoomSubList extends React.PureComponent {
chevron = (<div className={chevronClasses} />);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onClick={this.onClick}
className="mx_RoomSubList_label"
tabIndex={0}
aria-expanded={!isCollapsed}
inputRef={this._headerButton}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
return <RovingTabIndexWrapper inputRef={this._headerButton}>
{({onFocus, isActive, ref}) => {
const tabIndex = isActive ? 0 : -1;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
}
checkOverflow = () => {
@ -356,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = `${height}px`;
this._subList.current.style.height = toPx(height);
}
this._updateLazyRenderHeight(height);
};

View file

@ -25,26 +25,23 @@ import shouldHideEvent from '../../shouldHideEvent';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk";
import { _t } from '../../languageHandler';
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
import MatrixClientPeg from '../../MatrixClientPeg';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import sdk from '../../index';
import * as sdk from '../../index';
import CallHandler from '../../CallHandler';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import ObjectUtils from '../../ObjectUtils';
import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
@ -52,9 +49,12 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import WidgetUtils from '../../utils/WidgetUtils';
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
const DEBUG = false;
let debuglog = function() {};
@ -66,13 +66,7 @@ if (DEBUG) {
debuglog = console.log.bind(console);
}
const RoomContext = PropTypes.shape({
canReact: PropTypes.bool.isRequired,
canReply: PropTypes.bool.isRequired,
room: PropTypes.instanceOf(Room),
});
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomView',
propTypes: {
ConferenceHandler: PropTypes.any,
@ -103,8 +97,12 @@ module.exports = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string),
},
statics: {
contextType: MatrixClientContext,
},
getInitialState: function() {
const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled();
const llMembers = this.context.hasLazyLoadMembersEnabled();
return {
room: null,
roomId: null,
@ -137,6 +135,8 @@ module.exports = createReactClass({
isAlone: false,
isPeeking: false,
showingPinned: false,
showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@ -165,53 +165,42 @@ module.exports = createReactClass({
canReact: false,
canReply: false,
useCider: false,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
};
},
childContextTypes: {
room: RoomContext,
},
getChildContext: function() {
const {canReact, canReply, room} = this.state;
return {
room: {
canReact,
canReply,
room,
},
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.on("Room", this.onRoom);
this.context.on("Room.timeline", this.onRoomTimeline);
this.context.on("Room.name", this.onRoomName);
this.context.on("Room.accountData", this.onRoomAccountData);
this.context.on("RoomState.events", this.onRoomStateEvents);
this.context.on("RoomState.members", this.onRoomStateMember);
this.context.on("Room.myMembership", this.onMyMembership);
this.context.on("accountData", this.onAccountData);
this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
this._onRoomViewStoreUpdate(true);
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
this._onCiderUpdated();
this._ciderWatcherRef = SettingsStore.watchSetting(
"useCiderComposer", null, this._onCiderUpdated);
this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
this._onReadReceiptsChange);
this._roomView = createRef();
this._searchResultsPanel = createRef();
},
_onCiderUpdated: function() {
this.setState({useCider: SettingsStore.getValue("useCiderComposer")});
_onReadReceiptsChange: function() {
this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
});
},
_onRoomViewStoreUpdate: function(initial) {
@ -234,8 +223,10 @@ module.exports = createReactClass({
return;
}
const roomId = RoomViewStore.getRoomId();
const newState = {
roomId: RoomViewStore.getRoomId(),
roomId,
roomAlias: RoomViewStore.getRoomAlias(),
roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(),
@ -243,10 +234,17 @@ module.exports = createReactClass({
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
};
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
}
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
console.log(
'RVS update:',
@ -261,7 +259,7 @@ module.exports = createReactClass({
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
newState.room = this.context.getRoom(newState.roomId);
if (newState.room) {
newState.showApps = this._shouldShowApps(newState.room);
this._onRoomLoaded(newState.room);
@ -363,7 +361,7 @@ module.exports = createReactClass({
peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
});
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
this.context.peekInRoom(roomId).then((room) => {
if (this.unmounted) {
return;
}
@ -372,7 +370,7 @@ module.exports = createReactClass({
peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
}).catch((err) => {
if (this.unmounted) {
return;
}
@ -385,7 +383,7 @@ module.exports = createReactClass({
// This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') {
// This is fine: the room just isn't peekable (we assume).
this.setState({
peekLoading: false,
@ -395,10 +393,8 @@ module.exports = createReactClass({
}
});
} else if (room) {
//viewing a previously joined room, try to lazy load members
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking();
this.context.stopPeeking();
this.setState({isPeeking: false});
}
}
@ -412,13 +408,9 @@ module.exports = createReactClass({
const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") {
return false;
}
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
// This is confusing, but it means to say that we default to the tray being
// hidden unless the user clicked to open it.
return hideWidgetDrawer === "false";
},
componentDidMount: function() {
@ -436,22 +428,7 @@ module.exports = createReactClass({
}
this.onResize();
document.addEventListener("keydown", this.onKeyDown);
// XXX: EVIL HACK to autofocus inviting on empty rooms.
// We use the setTimeout to avoid racing with focus_composer.
if (this.state.room &&
this.state.room.getJoinedMemberCount() == 1 &&
this.state.room.getLiveTimeline() &&
this.state.room.getLiveTimeline().getEvents() &&
this.state.room.getLiveTimeline().getEvents().length <= 6) {
const inviteBox = document.getElementById("mx_SearchableEntityList_query");
setTimeout(function() {
if (inviteBox) {
inviteBox.focus();
}
}, 50);
}
document.addEventListener("keydown", this.onNativeKeyDown);
},
shouldComponentUpdate: function(nextProps, nextState) {
@ -461,7 +438,7 @@ module.exports = createReactClass({
componentDidUpdate: function() {
if (this._roomView.current) {
const roomView = ReactDOM.findDOMNode(this._roomView.current);
const roomView = this._roomView.current;
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
@ -490,13 +467,15 @@ module.exports = createReactClass({
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
SettingsStore.unwatchSetting(this._ciderWatcherRef);
// update the scroll map before we get unmounted
if (this.state.roomId) {
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
}
if (this.state.shouldPeek) {
this.context.stopPeeking();
}
// stop tracking room changes to format permalinks
this._stopAllPermalinkCreators();
@ -505,23 +484,26 @@ module.exports = createReactClass({
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
const roomView = ReactDOM.findDOMNode(this._roomView.current);
const roomView = this._roomView.current;
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
}
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
if (this.context) {
this.context.removeListener("Room", this.onRoom);
this.context.removeListener("Room.timeline", this.onRoomTimeline);
this.context.removeListener("Room.name", this.onRoomName);
this.context.removeListener("Room.accountData", this.onRoomAccountData);
this.context.removeListener("RoomState.events", this.onRoomStateEvents);
this.context.removeListener("Room.myMembership", this.onMyMembership);
this.context.removeListener("RoomState.members", this.onRoomStateMember);
this.context.removeListener("accountData", this.onAccountData);
this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -529,15 +511,24 @@ module.exports = createReactClass({
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
}
document.removeEventListener("keydown", this.onKeyDown);
document.removeEventListener("keydown", this.onNativeKeyDown);
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
// Remove RightPanelStore listener
if (this._rightPanelStoreToken) {
this._rightPanelStoreToken.remove();
}
WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
if (this._showReadReceiptsWatchRef) {
SettingsStore.unwatchSetting(this._showReadReceiptsWatchRef);
this._showReadReceiptsWatchRef = null;
}
// cancel any pending calls to the rate_limited_funcs
this._updateRoomMembers.cancelPendingCall();
@ -546,6 +537,12 @@ module.exports = createReactClass({
// Tinter.tint(); // reset colourscheme
},
_onRightPanelStoreUpdate: function() {
this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
});
},
onPageUnload(event) {
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
return event.returnValue =
@ -556,8 +553,8 @@ module.exports = createReactClass({
}
},
onKeyDown: function(ev) {
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
onNativeKeyDown: function(ev) {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
@ -583,12 +580,39 @@ module.exports = createReactClass({
}
},
onReactKeyDown: function(ev) {
let handled = false;
switch (ev.key) {
case Key.ESCAPE:
if (!ev.altKey && !ev.ctrlKey && !ev.shiftKey && !ev.metaKey) {
this._messagePanel.forgetReadMarker();
this.jumpToLiveTimeline();
handled = true;
}
break;
case Key.PAGE_UP:
if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) {
this.jumpToReadMarker();
handled = true;
}
break;
case Key.U.toUpperCase():
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) {
dis.dispatch({ action: "upload_file" })
handled = true;
}
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
},
onAction: function(payload) {
switch (payload.action) {
case 'after_right_panel_phase_change':
// We don't keep state on the right panel, so just re-render to update
this.forceUpdate();
break;
case 'message_send_failed':
case 'message_sent':
this._checkIfAlone(this.state.room);
@ -600,9 +624,7 @@ module.exports = createReactClass({
payload.data.description || payload.data.name);
break;
case 'picture_snapshot':
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
);
ContentMessages.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context);
break;
case 'notifier_enabled':
case 'upload_started':
@ -646,6 +668,32 @@ module.exports = createReactClass({
this.onCancelSearchClick();
}
break;
case 'quote':
if (this.state.searchResults) {
const roomId = payload.event.getRoomId();
if (roomId === this.state.roomId) {
this.onCancelSearchClick();
}
setImmediate(() => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
deferred_action: payload,
});
});
}
break;
case 'sync_state':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this._onRoomViewStoreUpdate(true);
});
}
break;
}
},
@ -675,7 +723,7 @@ module.exports = createReactClass({
// we'll only be showing a spinner.
if (this.state.joining) return;
if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) {
if (ev.getSender() !== this.context.credentials.userId) {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
@ -731,8 +779,7 @@ module.exports = createReactClass({
_loadMembersIfJoined: async function(room) {
// lazy load members if enabled
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
if (this.context.hasLazyLoadMembersEnabled()) {
if (room && room.getMyMembership() === 'join') {
try {
await room.loadMembersIfNeeded();
@ -767,7 +814,7 @@ module.exports = createReactClass({
_updatePreviewUrlVisibility: function({roomId}) {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
this.setState({
showUrlPreview: SettingsStore.getValue(key, roomId),
});
@ -792,12 +839,26 @@ module.exports = createReactClass({
this._updateE2EStatus(room);
},
_updateE2EStatus: async function(room) {
const cli = MatrixClientPeg.get();
if (!cli.isRoomEncrypted(room.roomId)) {
onUserVerificationChanged: function(userId, _trustStatus) {
const room = this.state.room;
if (!room || !room.currentState.getMember(userId)) {
return;
}
if (!cli.isCryptoEnabled()) {
this._updateE2EStatus(room);
},
onCrossSigningKeysChanged: function() {
const room = this.state.room;
if (room) {
this._updateE2EStatus(room);
}
},
_updateE2EStatus: async function(room) {
if (!this.context.isRoomEncrypted(room.roomId)) {
return;
}
if (!this.context.isCryptoEnabled()) {
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
@ -806,38 +867,19 @@ module.exports = createReactClass({
});
return;
}
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
});
});
debuglog("e2e check is warning/verified only as cross-signing is off");
return;
}
const e2eMembers = await room.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const { userId } = member;
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
if (!userVerified) {
this.setState({
e2eStatus: "warning",
});
return;
}
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(device => {
const { deviceId } = device;
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
});
if (!allDevicesVerified) {
this.setState({
e2eStatus: "warning",
});
return;
}
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
e2eStatus: "verified",
e2eStatus: await shieldStatusForRoom(this.context, room),
});
},
@ -906,7 +948,7 @@ module.exports = createReactClass({
_updatePermissions: function(room) {
if (room) {
const me = MatrixClientPeg.get().getUserId();
const me = this.context.getUserId();
const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me);
const canReply = room.maySendMessage();
@ -916,7 +958,7 @@ module.exports = createReactClass({
// rate limited because a power level change will emit an event for every
// member in the room.
_updateRoomMembers: new rate_limited_func(function(dueToMember) {
_updateRoomMembers: rate_limited_func(function(dueToMember) {
// a member state changed in this room
// refresh the conf call notification state
this._updateConfCallNotification();
@ -990,7 +1032,7 @@ module.exports = createReactClass({
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
const searchPromise = this.context.backPaginateRoomEventsSearch(
this.state.searchResults);
return this._handleSearchResult(searchPromise);
} else {
@ -1016,10 +1058,8 @@ module.exports = createReactClass({
},
onJoinButtonClicked: function(ev) {
const cli = MatrixClientPeg.get();
// If the user is a ROU, allow them to transition to a PWLU
if (cli && cli.isGuest()) {
if (this.context && this.context.isGuest()) {
// Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.)
dis.dispatch({
@ -1116,7 +1156,7 @@ module.exports = createReactClass({
ev.stopPropagation();
ev.preventDefault();
ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
this.setState({ draggingFile: false });
dis.dispatch({action: 'focus_composer'});
@ -1129,12 +1169,12 @@ module.exports = createReactClass({
},
injectSticker: function(url, info, text) {
if (MatrixClientPeg.get().isGuest()) {
if (this.context.isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
}
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context)
.then(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
@ -1212,7 +1252,7 @@ module.exports = createReactClass({
});
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error);
console.error("Search failed", error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
@ -1225,12 +1265,9 @@ module.exports = createReactClass({
},
getSearchResultTiles: function() {
const EventTile = sdk.getComponent('rooms.EventTile');
const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
const Spinner = sdk.getComponent("elements.Spinner");
const cli = MatrixClientPeg.get();
// XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work?
@ -1238,21 +1275,21 @@ module.exports = createReactClass({
if (this.state.searchInProgress) {
ret.push(<li key="search-spinner">
<Spinner />
</li>);
<Spinner />
</li>);
}
if (!this.state.searchResults.next_batch) {
if (this.state.searchResults.results.length == 0) {
ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li>,
);
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li>,
);
} else {
ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li>,
);
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li>,
);
}
}
@ -1272,9 +1309,9 @@ module.exports = createReactClass({
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = cli.getRoom(roomId);
const room = this.context.getRoom(roomId);
if (!EventTile.haveTileForEvent(mxEv)) {
if (!haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
// not match the displayed count.
continue;
@ -1339,7 +1376,7 @@ module.exports = createReactClass({
},
onForgetClick: function() {
MatrixClientPeg.get().forget(this.state.room.roomId).then(function() {
this.context.forget(this.state.room.roomId).then(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
const errCode = err.errcode || _t("unknown error code");
@ -1356,7 +1393,7 @@ module.exports = createReactClass({
this.setState({
rejecting: true,
});
MatrixClientPeg.get().leave(this.state.roomId).then(function() {
this.context.leave(this.state.roomId).then(function() {
dis.dispatch({ action: 'view_next_room' });
self.setState({
rejecting: false,
@ -1378,6 +1415,40 @@ module.exports = createReactClass({
});
},
onRejectAndIgnoreClick: async function() {
this.setState({
rejecting: true,
});
try {
const myMember = this.state.room.getMember(this.context.getUserId());
const inviteEvent = myMember.events.member;
const ignoredUsers = this.context.getIgnoredUsers();
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
await this.context.setIgnoredUsers(ignoredUsers);
await this.context.leave(this.state.roomId);
dis.dispatch({ action: 'view_next_room' });
this.setState({
rejecting: false,
});
} catch (error) {
console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"),
description: msg,
});
self.setState({
rejecting: false,
rejectError: error,
});
}
},
onRejectThreepidInviteButtonClicked: function(ev) {
// We can reject 3pid invites in the same way that we accept them,
// using /leave rather than /join. In the short term though, we
@ -1575,7 +1646,7 @@ module.exports = createReactClass({
const createEvent = this.state.room.currentState.getStateEvents("m.room.create", "");
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']);
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
},
_getHiddenHighlightCount: function() {
@ -1605,14 +1676,16 @@ module.exports = createReactClass({
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading;
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
if (loading) {
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
previewLoading={this.state.peekLoading}
previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
@ -1637,7 +1710,8 @@ module.exports = createReactClass({
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
@ -1669,10 +1743,13 @@ module.exports = createReactClass({
</ErrorBoundary>
);
} else {
const myUserId = MatrixClientPeg.get().credentials.userId;
const myUserId = this.context.credentials.userId;
const myMember = this.state.room.getMember(myUserId);
const inviteEvent = myMember.events.member;
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
const inviteEvent = myMember ? myMember.events.member : null;
let inviterName = _t("Unknown");
if (inviteEvent) {
inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
}
// We deliberately don't try to peek into invites, even if we have permission to peek
// as they could be a spam vector.
@ -1682,9 +1759,11 @@ module.exports = createReactClass({
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
onRejectAndIgnoreClick={this.onRejectAndIgnoreClick}
inviterName={inviterName}
canPreview={false}
joining={this.state.joining}
@ -1734,13 +1813,13 @@ module.exports = createReactClass({
const showRoomUpgradeBar = (
roomVersionRecommendation &&
roomVersionRecommendation.needsUpgrade &&
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
);
const showRoomRecoveryReminder = (
SettingsStore.getValue("showRoomRecoveryReminder") &&
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
!MatrixClientPeg.get().getKeyBackupEnabled()
this.context.isRoomEncrypted(this.state.room.roomId) &&
this.context.getKeyBackupEnabled() === false
);
const hiddenHighlightCount = this._getHiddenHighlightCount();
@ -1811,7 +1890,7 @@ module.exports = createReactClass({
const auxPanel = (
<AuxPanel room={this.state.room}
fullHeight={false}
userId={MatrixClientPeg.get().credentials.userId}
userId={this.context.credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
@ -1828,29 +1907,16 @@ module.exports = createReactClass({
myMembership === 'join' && !this.state.searchResults
);
if (canSpeak) {
if (this.state.useCider) {
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
messageComposer =
<MessageComposer
room={this.state.room}
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
/>;
} else {
const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer');
messageComposer =
<SlateMessageComposer
room={this.state.room}
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
/>;
}
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
messageComposer =
<MessageComposer
room={this.state.room}
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
/>;
}
// TODO: Why aren't we storing the term/scope/count in this format
@ -1935,7 +2001,7 @@ module.exports = createReactClass({
<TimelinePanel
ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
@ -1953,7 +2019,8 @@ module.exports = createReactClass({
/>);
let topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) {
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = (<TopUnreadMessagesBar
onScrollUpClick={this.jumpToReadMarker}
@ -1961,7 +2028,8 @@ module.exports = createReactClass({
/>);
}
let jumpToBottom;
if (!this.state.atEndOfLiveTimeline) {
// Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton
numUnreadMessages={this.state.numUnreadMessages}
@ -1982,54 +2050,61 @@ module.exports = createReactClass({
},
);
const showRightPanel = !forceHideRightPanel && this.state.room
&& RightPanelStore.getSharedInstance().isOpenForRoom;
const showRightPanel = !forceHideRightPanel && this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
: null;
const timelineClasses = classNames("mx_RoomView_timeline", {
mx_RoomView_timeline_rr_enabled: this.state.showReadReceipts,
});
const mainClasses = classNames("mx_RoomView", {
mx_RoomView_inCall: inCall,
});
return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
<ErrorBoundary>
<RoomHeader
room={this.state.room}
searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{auxPanel}
<div className="mx_RoomView_timeline">
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
<RoomContext.Provider value={this.state}>
<main className={mainClasses} ref={this._roomView} onKeyDown={this.onReactKeyDown}>
<ErrorBoundary>
<RoomHeader
room={this.state.room}
searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{auxPanel}
<div className={timelineClasses}>
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
{statusBar}
</div>
</div>
{previewBar}
{messageComposer}
</div>
{previewBar}
{messageComposer}
</div>
</MainSplit>
</ErrorBoundary>
</main>
</MainSplit>
</ErrorBoundary>
</main>
</RoomContext.Provider>
);
},
});
module.exports.RoomContext = RoomContext;

View file

@ -84,7 +84,7 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
module.exports = createReactClass({
export default createReactClass({
displayName: 'ScrollPanel',
propTypes: {
@ -144,6 +144,11 @@ module.exports = createReactClass({
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier: PropTypes.object,
/* fixedChildren: allows for children to be passed which are rendered outside
* of the wrapper
*/
fixedChildren: PropTypes.node,
},
getDefaultProps: function() {
@ -156,9 +161,8 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
this._fillRequestWhileRunning = false;
this._isFilling = false;
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._pendingFillRequests = {b: null, f: null};
if (this.props.resizeNotifier) {
@ -523,7 +527,7 @@ module.exports = createReactClass({
scrollRelative: function(mult) {
const scrollNode = this._getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.5;
scrollNode.scrollTop = scrollNode.scrollTop + delta;
scrollNode.scrollBy(0, delta);
this._saveScrollState();
},
@ -705,17 +709,15 @@ module.exports = createReactClass({
// the currently filled piece of the timeline
if (trackedNode) {
const oldTop = trackedNode.offsetTop;
// changing the height might change the scrollTop
// if the new height is smaller than the scrollTop.
// We calculate the diff that needs to be applied
// ourselves, so be sure to measure the
// scrollTop before changing the height.
const preexistingScrollTop = sn.scrollTop;
itemlist.style.height = `${newHeight}px`;
const newTop = trackedNode.offsetTop;
const topDiff = newTop - oldTop;
sn.scrollTop = preexistingScrollTop + topDiff;
debuglog("updateHeight to", {newHeight, topDiff, preexistingScrollTop});
// important to scroll by a relative amount as
// reading scrollTop and then setting it might
// yield out of date values and cause a jump
// when setting it
sn.scrollBy(0, topDiff);
debuglog("updateHeight to", {newHeight, topDiff});
}
}
},
@ -767,6 +769,7 @@ module.exports = createReactClass({
},
_topFromBottom(node) {
// current capped height - distance from top = distance from bottom of container to top of tracked element
return this._itemlist.current.clientHeight - node.offsetTop;
},
@ -783,7 +786,7 @@ module.exports = createReactClass({
if (!this._divScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
}
return this._divScroll;
@ -877,11 +880,15 @@ module.exports = createReactClass({
// TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway.
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
// list-style-type: none; is no longer a list
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>

View file

@ -19,12 +19,12 @@ import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
module.exports = createReactClass({
export default createReactClass({
displayName: 'SearchBox',
propTypes: {
@ -53,6 +53,7 @@ module.exports = createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._search = createRef();
},
@ -133,9 +134,11 @@ module.exports = createReactClass({
return null;
}
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
(<AccessibleButton key="button"
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
(<AccessibleButton
key="button"
tabIndex={-1}
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
</AccessibleButton>) : undefined;
// show a shorter placeholder when blurred, if requested
@ -158,6 +161,7 @@ module.exports = createReactClass({
onKeyDown={ this._onKeyDown }
onBlur={this._onBlur}
placeholder={ placeholder }
autoComplete="off"
/>
{ clearButton }
</div>

View file

@ -1,7 +1,7 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,41 +18,55 @@ limitations under the License.
import * as React from "react";
import {_t} from '../../languageHandler';
import PropTypes from "prop-types";
import sdk from "../../index";
import * as PropTypes from "prop-types";
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import { ReactNode } from "react";
/**
* Represents a tab for the TabbedView.
*/
export class Tab {
public label: string;
public icon: string;
public body: React.ReactNode;
/**
* Creates a new tab.
* @param {string} tabLabel The untranslated tab label.
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
* @param {string} tabJsx The JSX for the tab container.
* @param {React.ReactNode} tabJsx The JSX for the tab container.
*/
constructor(tabLabel, tabIconClass, tabJsx) {
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
this.label = tabLabel;
this.icon = tabIconClass;
this.body = tabJsx;
}
}
export class TabbedView extends React.Component {
interface IProps {
tabs: Tab[];
}
interface IState {
activeTabIndex: number;
}
export default class TabbedView extends React.Component<IProps, IState> {
static propTypes = {
// The tabs to show
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
};
constructor() {
super();
constructor(props: IProps) {
super(props);
this.state = {
activeTabIndex: 0,
};
}
_getActiveTabIndex() {
private _getActiveTabIndex() {
if (!this.state || !this.state.activeTabIndex) return 0;
return this.state.activeTabIndex;
}
@ -62,7 +76,7 @@ export class TabbedView extends React.Component {
* @param {Tab} tab the tab to show
* @private
*/
_setActiveTab(tab) {
private _setActiveTab(tab: Tab) {
const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) {
this.setState({activeTabIndex: idx});
@ -71,7 +85,7 @@ export class TabbedView extends React.Component {
}
}
_renderTabLabel(tab) {
private _renderTabLabel(tab: Tab) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let classes = "mx_TabbedView_tabLabel ";
@ -97,17 +111,17 @@ export class TabbedView extends React.Component {
);
}
_renderTabPanel(tab) {
private _renderTabPanel(tab: Tab): React.ReactNode {
return (
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
<div className='mx_TabbedView_tabPanelContent'>
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
{tab.body}
</div>
</AutoHideScrollbar>
</div>
);
}
render() {
public render(): React.ReactNode {
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);

View file

@ -1,5 +1,6 @@
/*
Copyright 2017, 2018 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,24 +17,24 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
import sdk from '../../index';
import dis from '../../dispatcher';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
const TagPanel = createReactClass({
displayName: 'TagPanel',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
statics: {
contextType: MatrixClientContext,
},
getInitialState() {
@ -43,10 +44,10 @@ const TagPanel = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.on("sync", this._onClientSync);
this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync);
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
@ -58,21 +59,21 @@ const TagPanel = createReactClass({
});
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.removeListener("sync", this._onClientSync);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.removeListener("sync", this._onClientSync);
if (this._tagOrderStoreToken) {
this._tagOrderStoreToken.remove();
}
},
_onGroupMyMembership() {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
},
_onClientSync(syncState, prevState) {
@ -81,7 +82,7 @@ const TagPanel = createReactClass({
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
// Load joined groups
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
}
},
@ -104,8 +105,8 @@ const TagPanel = createReactClass({
render() {
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const ActionButton = sdk.getComponent('elements.ActionButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -137,9 +138,8 @@ const TagPanel = createReactClass({
{ clearButton }
</div>
<div className="mx_TagPanel_divider" />
<GeminiScrollbarWrapper
<AutoHideScrollbar
className="mx_TagPanel_scroller"
autoshow={true}
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
onMouseDown={this.onMouseDown}
@ -154,11 +154,18 @@ const TagPanel = createReactClass({
ref={provided.innerRef}
>
{ tags }
<div>
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
</div>
{ provided.placeholder }
</div>
) }
</Droppable>
</GeminiScrollbarWrapper>
</AutoHideScrollbar>
</div>;
},
});

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../index';
import dis from '../../dispatcher';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';

View file

@ -2,7 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019-2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,26 +18,24 @@ limitations under the License.
*/
import SettingsStore from "../../settings/SettingsStore";
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
const Matrix = require("matrix-js-sdk");
const EventTimeline = Matrix.EventTimeline;
const sdk = require('../../index');
import {EventTimeline} from "matrix-js-sdk";
import * as Matrix from "matrix-js-sdk";
import { _t } from '../../languageHandler';
const MatrixClientPeg = require("../../MatrixClientPeg");
const dis = require("../../dispatcher");
const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity");
import {Key} from '../../Keyboard';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as ObjectUtils from "../../ObjectUtils";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
import * as sdk from "../../index";
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -96,6 +94,10 @@ const TimelinePanel = createReactClass({
// callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: PropTypes.func,
// callback which is called when we wish to paginate the timeline
// window.
onPaginationRequest: PropTypes.func,
// maximum number of events to show in a timeline
timelineCap: PropTypes.number,
@ -144,6 +146,9 @@ const TimelinePanel = createReactClass({
liveEvents: [],
timelineLoading: true, // track whether our room timeline is loading
// the index of the first event that is to be shown
firstVisibleEventIndex: 0,
// canBackPaginate == false may mean:
//
// * we haven't (successfully) loaded the timeline yet, or:
@ -197,7 +202,8 @@ const TimelinePanel = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
debuglog("TimelinePanel: mounting");
this.lastRRSentEventId = undefined;
@ -229,7 +235,8 @@ const TimelinePanel = createReactClass({
this._initTimeline(this.props);
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
@ -331,15 +338,24 @@ const TimelinePanel = createReactClass({
// We can now paginate in the unpaginated direction
const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
const { events, liveEvents } = this._getEvents();
const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
this.setState({
[canPaginateKey]: true,
events,
liveEvents,
firstVisibleEventIndex,
});
}
},
onPaginationRequest(timelineWindow, direction, size) {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
return timelineWindow.paginate(direction, size);
}
},
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
if (!this._shouldPaginate()) return Promise.resolve(false);
@ -359,20 +375,26 @@ const TimelinePanel = createReactClass({
return Promise.resolve(false);
}
if (backwards && this.state.firstVisibleEventIndex !== 0) {
debuglog("TimelinePanel: won't", dir, "paginate past first visible event");
return Promise.resolve(false);
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
this.setState({[paginatingKey]: true});
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
const { events, liveEvents } = this._getEvents();
const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
const newState = {
[paginatingKey]: false,
[canPaginateKey]: r,
events,
liveEvents,
firstVisibleEventIndex,
};
// moving the window in this direction may mean that we can now
@ -392,7 +414,11 @@ const TimelinePanel = createReactClass({
// itself into the right place
return new Promise((resolve) => {
this.setState(newState, () => {
resolve(r);
// we can continue paginating in the given direction if:
// - _timelineWindow.paginate says we can
// - we're paginating forwards, or we won't be trying to
// paginate backwards past the first visible event
resolve(r && (!backwards || firstVisibleEventIndex === 0));
});
});
});
@ -466,12 +492,13 @@ const TimelinePanel = createReactClass({
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
const { events, liveEvents } = this._getEvents();
const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
const lastLiveEvent = liveEvents[liveEvents.length - 1];
const updatedState = {
events,
liveEvents,
firstVisibleEventIndex,
};
let callRMUpdated;
@ -1105,6 +1132,7 @@ const TimelinePanel = createReactClass({
// get the list of events from the timeline window and the pending event list
_getEvents: function() {
const events = this._timelineWindow.getEvents();
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker
// should use this list, so that they don't advance into pending events.
@ -1118,9 +1146,84 @@ const TimelinePanel = createReactClass({
return {
events,
liveEvents,
firstVisibleEventIndex,
};
},
/**
* Check for undecryptable messages that were sent while the user was not in
* the room.
*
* @param {Array<MatrixEvent>} events The timeline events to check
*
* @return {Number} The index within `events` of the event after the most recent
* undecryptable event that was sent while the user was not in the room. If no
* such events were found, then it returns 0.
*/
_checkForPreJoinUISI: function(events) {
const room = this.props.timelineSet.room;
if (events.length === 0 || !room ||
!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
return 0;
}
const userId = MatrixClientPeg.get().credentials.userId;
// get the user's membership at the last event by getting the timeline
// that the event belongs to, and traversing the timeline looking for
// that event, while keeping track of the user's membership
let i;
let userMembership = "leave";
for (i = events.length - 1; i >= 0; i--) {
const timeline = room.getTimelineForEvent(events[i].getId());
if (!timeline) {
// Somehow, it seems to be possible for live events to not have
// a timeline, even though that should not happen. :(
// https://github.com/vector-im/riot-web/issues/12120
console.warn(
`Event ${events[i].getId()} in room ${room.roomId} is live, ` +
`but it does not have a timeline`,
);
continue;
}
const userMembershipEvent =
timeline.getState(EventTimeline.FORWARDS).getMember(userId);
userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave";
const timelineEvents = timeline.getEvents();
for (let j = timelineEvents.length - 1; j >= 0; j--) {
const event = timelineEvents[j];
if (event.getId() === events[i].getId()) {
break;
} else if (event.getStateKey() === userId
&& event.getType() === "m.room.member") {
const prevContent = event.getPrevContent();
userMembership = prevContent.membership || "leave";
}
}
break;
}
// now go through the rest of the events and find the first undecryptable
// one that was sent when the user wasn't in the room
for (; i >= 0; i--) {
const event = events[i];
if (event.getStateKey() === userId
&& event.getType() === "m.room.member") {
const prevContent = event.getPrevContent();
userMembership = prevContent.membership || "leave";
} else if (userMembership === "leave" &&
(event.isDecryptionFailure() || event.isBeingDecrypted())) {
// reached an undecryptable message when the user wasn't in
// the room -- don't try to load any more
// Note: for now, we assume that events that are being decrypted are
// not decryptable
return i + 1;
}
}
return 0;
},
_indexForEventId: function(evId) {
for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
@ -1136,11 +1239,11 @@ const TimelinePanel = createReactClass({
const allowPartial = opts.allowPartial || false;
const messagePanel = this._messagePanel.current;
if (messagePanel === undefined) return null;
if (!messagePanel) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
const isNodeInView = (node) => {
@ -1181,7 +1284,7 @@ const TimelinePanel = createReactClass({
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
const isWithoutTile = !EventTile.haveTileForEvent(ev) || shouldHideEvent(ev);
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
if (isWithoutTile || !node) {
// don't start counting if the event should be ignored,
@ -1313,6 +1416,9 @@ const TimelinePanel = createReactClass({
this.state.forwardPaginating ||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
);
const events = this.state.firstVisibleEventIndex
? this.state.events.slice(this.state.firstVisibleEventIndex)
: this.state.events;
return (
<MessagePanel
ref={this._messagePanel}
@ -1321,7 +1427,7 @@ const TimelinePanel = createReactClass({
hidden={this.props.hidden}
backPaginating={this.state.backPaginating}
forwardPaginating={forwardPaginating}
events={this.state.events}
events={events}
highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={this.state.readMarkerVisible}
@ -1346,4 +1452,4 @@ const TimelinePanel = createReactClass({
},
});
module.exports = TimelinePanel;
export default TimelinePanel;

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,37 +15,38 @@ limitations under the License.
*/
import * as React from "react";
import dis from "../../dispatcher";
import { _t } from '../../languageHandler';
import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames";
export default class ToastContainer extends React.Component {
constructor() {
super();
this.state = {toasts: []};
}
interface IState {
toasts: IToast<any>[];
countSeen: number;
}
componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
this.state = {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
// Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find
// they're already irrelevant by the time they're mounted, and
// our own componentDidMount is too late.
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
}
onAction = (payload) => {
if (payload.action === "show_toast") {
this._addToast(payload.toast);
}
};
_addToast(toast) {
this.setState({toasts: this.state.toasts.concat(toast)});
}
dismissTopToast = () => {
const [, ...remaining] = this.state.toasts;
this.setState({toasts: remaining});
_onToastStoreUpdate = () => {
this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
};
render() {
@ -59,14 +60,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
});
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
let countIndicator;
if (isStacked || this.state.countSeen > 0) {
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
}
const toastProps = Object.assign({}, props, {
dismiss: this.dismissTopToast,
key,
toastKey: key,
});
toast = (<div className={toastClasses}>
<h2>{title}{countIndicator}</h2>
<div className="mx_Toast_title">
<h2>{title}</h2>
<span>{countIndicator}</span>
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
}

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {TopLeftMenu} from '../views/context_menus/TopLeftMenu';
import TopLeftMenu from '../views/context_menus/TopLeftMenu';
import BaseAvatar from '../views/avatars/BaseAvatar';
import MatrixClientPeg from '../../MatrixClientPeg';
import Avatar from '../../Avatar';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
const AVATAR_SIZE = 28;

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,11 +19,11 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages';
const dis = require('../../dispatcher');
const filesize = require('filesize');
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
module.exports = createReactClass({
export default createReactClass({
displayName: 'UploadBar',
propTypes: {
room: PropTypes.object,

View file

@ -18,10 +18,11 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import Matrix from "matrix-js-sdk";
import MatrixClientPeg from "../../MatrixClientPeg";
import sdk from "../../index";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
export default class UserView extends React.Component {
static get propTypes() {
@ -35,14 +36,17 @@ export default class UserView extends React.Component {
this.state = {};
}
componentWillMount() {
componentDidMount() {
if (this.props.userId) {
this._loadProfileInfo();
}
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
// XXX: We shouldn't need to null check the userId here, but we declare
// it as optional and MatrixChat sometimes fires in a way which results
// in an NPE when we try to update the profile info.
if (prevProps.userId !== this.props.userId && this.props.userId) {
this._loadProfileInfo();
}
}
@ -76,7 +80,7 @@ export default class UserView extends React.Component {
const RightPanel = sdk.getComponent('structures.RightPanel');
const MainSplit = sdk.getComponent('structures.MainSplit');
const panel = <RightPanel user={this.state.member} />;
return (<MainSplit panel={panel}><div style={{flex: "1"}} /></MainSplit>);
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
} else {
return (<div />);
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,10 +21,10 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
import {_t} from "../../languageHandler";
import sdk from "../../index";
import * as sdk from "../../index";
module.exports = createReactClass({
export default createReactClass({
displayName: 'ViewSource',
propTypes: {

View file

@ -0,0 +1,91 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
export default class CompleteSecurity extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {phase: store.phase};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({phase: store.phase});
};
componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const {phase} = this.state;
let icon;
let title;
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");
} else if (phase === PHASE_CONFIRM_SKIP) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Are you sure?");
} else if (phase === PHASE_BUSY) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else {
throw new Error(`Unknown phase ${phase}`);
}
return (
<AuthPage>
<CompleteSecurityBody>
<h2 className="mx_CompleteSecurity_header">
{icon}
{title}
</h2>
<div className="mx_CompleteSecurity_body">
<SetupEncryptionBody onFinished={this.props.onFinished} />
</div>
</CompleteSecurityBody>
</AuthPage>
);
}
}

View file

@ -0,0 +1,50 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 AsyncWrapper from '../../../AsyncWrapper';
import * as sdk from '../../../index';
export default class E2eSetup extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
accountPassword: PropTypes.string,
};
constructor() {
super();
// awkwardly indented because https://github.com/eslint/eslint/issues/11310
this._createStorageDialogPromise =
import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog");
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
return (
<AuthPage>
<CompleteSecurityBody>
<AsyncWrapper prom={this._createStorageDialogPromise}
hasCancel={false}
onFinished={this.props.onFinished}
accountPassword={this.props.accountPassword}
/>
</CompleteSecurityBody>
</AuthPage>
);
}
}

View file

@ -20,12 +20,13 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import * as sdk from '../../../index';
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage";
// Phases
// Show controls to configure server details
@ -39,7 +40,7 @@ const PHASE_EMAIL_SENT = 3;
// User has clicked the link in email and completed reset
const PHASE_DONE = 4;
module.exports = createReactClass({
export default createReactClass({
displayName: 'ForgotPassword',
propTypes: {
@ -68,12 +69,13 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.reset = null;
this._checkServerLiveliness(this.props.serverConfig);
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -151,8 +153,8 @@ module.exports = createReactClass({
<div>
{ _t(
"Changing your password will reset any end-to-end encryption keys " +
"on all of your devices, making encrypted chat history unreadable. Set up " +
"Key Backup or export your room keys from another device before resetting your " +
"on all of your sessions, making encrypted chat history unreadable. Set up " +
"Key Backup or export your room keys from another session before resetting your " +
"password.",
) }
</div>,
@ -295,7 +297,6 @@ module.exports = createReactClass({
<form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow">
<Field
id="mx_ForgotPassword_email"
name="reset_email" // define a name so browser's password autofill gets less confused
type="text"
label={_t('Email')}
@ -306,7 +307,6 @@ module.exports = createReactClass({
</div>
<div className="mx_AuthBody_fieldRow">
<Field
id="mx_ForgotPassword_password"
name="reset_password"
type="password"
label={_t('Password')}
@ -314,7 +314,6 @@ module.exports = createReactClass({
onChange={this.onInputChanged.bind(this, "password")}
/>
<Field
id="mx_ForgotPassword_passwordConfirm"
name="reset_password_confirm"
type="password"
label={_t('Confirm')}
@ -357,7 +356,7 @@ module.exports = createReactClass({
return <div>
<p>{_t("Your password has been reset.")}</p>
<p>{_t(
"You have been logged out of all devices and will no longer receive " +
"You have been logged out of all sessions and will no longer receive " +
"push notifications. To re-enable notifications, sign in again on each " +
"device.",
)}</p>
@ -367,7 +366,6 @@ module.exports = createReactClass({
},
render: function() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");

View file

@ -20,12 +20,15 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../languageHandler';
import sdk from '../../../index';
import * as sdk from '../../../index';
import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -53,10 +56,15 @@ _td("General failure");
/**
* A wire component which glues together login UI components and Login logic
*/
module.exports = createReactClass({
export default createReactClass({
displayName: 'Login',
propTypes: {
// Called when the user has logged in. Params:
// - The object returned by the login API
// - The user's password, if applicable, (may be cached in memory for a
// short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn: PropTypes.func.isRequired,
// If true, the component will consider itself busy.
@ -76,11 +84,13 @@ module.exports = createReactClass({
onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
isSyncing: PropTypes.bool,
},
getInitialState: function() {
return {
busy: false,
busyLoggingIn: null,
errorText: null,
loginIncorrect: false,
canTryLogin: true, // can we attempt to log in or are there validation errors?
@ -105,7 +115,8 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
// map from login step type to a function which will render a control
@ -114,8 +125,8 @@ module.exports = createReactClass({
'm.login.password': this._renderPasswordStep,
// CAS and SSO are the same thing, modulo the url we link to
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
'm.login.cas': () => this._renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"),
};
this._initLoginLogic();
@ -125,7 +136,8 @@ module.exports = createReactClass({
this._unmounted = true;
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -159,6 +171,7 @@ module.exports = createReactClass({
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
this.setState({
busy: false,
busyLoggingIn: false,
...componentState,
});
aliveAgain = !componentState.serverErrorIsFatal;
@ -172,6 +185,7 @@ module.exports = createReactClass({
this.setState({
busy: true,
busyLoggingIn: true,
errorText: null,
loginIncorrect: false,
});
@ -180,7 +194,7 @@ module.exports = createReactClass({
username, phoneCountry, phoneNumber, password,
).then((data) => {
this.setState({serverIsAlive: true}); // it must be, we logged in.
this.props.onLoggedIn(data);
this.props.onLoggedIn(data, password);
}, (error) => {
if (this._unmounted) {
return;
@ -239,6 +253,8 @@ module.exports = createReactClass({
}
this.setState({
busy: false,
busyLoggingIn: false,
errorText: errorText,
// 401 would be the sensible status code for 'incorrect password'
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
@ -246,13 +262,6 @@ module.exports = createReactClass({
// We treat both as an incorrect password
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
});
}).finally(() => {
if (this._unmounted) {
return;
}
this.setState({
busy: false,
});
});
},
@ -338,6 +347,22 @@ module.exports = createReactClass({
this.props.onRegisterClick();
},
onTryRegisterClick: function(ev) {
const step = this._getCurrentFlowStep();
if (step === 'm.login.sso' || step === 'm.login.cas') {
// If we're showing SSO it means that registration is also probably disabled,
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
ev.preventDefault();
ev.stopPropagation();
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind,
this.props.fragmentAfterLogin);
} else {
// Don't intercept - just go through to the register page
this.onRegisterClick(ev);
}
},
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_LOGIN,
@ -475,7 +500,7 @@ module.exports = createReactClass({
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener"
return <a target="_blank" rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
>
{ sub }
@ -490,11 +515,10 @@ module.exports = createReactClass({
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
"is not blocking requests.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
'a': (sub) =>
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
{ sub }
</a>;
},
</a>,
},
) }
</span>;
@ -576,11 +600,12 @@ module.exports = createReactClass({
loginIncorrect={this.state.loginIncorrect}
serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()}
busy={this.props.isSyncing || this.state.busyLoggingIn}
/>
);
},
_renderSsoStep: function(url) {
_renderSsoStep: function(loginType) {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null;
@ -601,17 +626,23 @@ module.exports = createReactClass({
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} />
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
<SSOButton
className="mx_Login_sso_link mx_Login_submit"
matrixClient={this._loginLogic.createTemporaryClient()}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
</div>
);
},
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const AuthPage = sdk.getComponent("auth.AuthPage");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const loader = this.isBusy() && !this.state.busyLoggingIn ?
<div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.state.errorText;
@ -638,9 +669,28 @@ module.exports = createReactClass({
);
}
let footer;
if (this.props.isSyncing || this.state.busyLoggingIn) {
footer = <div className="mx_AuthBody_paddedFooter">
<div className="mx_AuthBody_paddedFooter_title">
<InlineSpinner w={20} h={20} />
{ this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
</div>
{ this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
{_t("If you've joined lots of rooms, this might take a while")}
</div> }
</div>;
} else {
footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }
</a>
);
}
return (
<AuthPage>
<AuthHeader />
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
<AuthBody>
<h2>
{_t('Sign in')}
@ -650,9 +700,7 @@ module.exports = createReactClass({
{ serverDeadSection }
{ this.renderServerComponent() }
{ this.renderLoginComponentForStep() }
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
{ _t('Create account') }
</a>
{ footer }
</AuthBody>
</AuthPage>
);

View file

@ -17,11 +17,12 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import AuthPage from "../../views/auth/AuthPage";
module.exports = createReactClass({
export default createReactClass({
displayName: 'PostRegistration',
propTypes: {
@ -36,7 +37,7 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
// the URL to be passed to you (because it's also used for room avatars).
@ -59,7 +60,6 @@ module.exports = createReactClass({
render: function() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const AuthPage = sdk.getComponent('auth.AuthPage');
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
return (

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import Matrix from 'matrix-js-sdk';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
@ -29,7 +29,10 @@ import * as ServerType from '../../views/auth/ServerTypeSelector';
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
// Phases
// Show controls to configure server details
@ -40,11 +43,17 @@ const PHASE_REGISTRATION = 1;
// Enable phases for registration
const PHASES_ENABLED = true;
module.exports = createReactClass({
export default createReactClass({
displayName: 'Registration',
propTypes: {
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
// - The user's password, if available and applicable (may be cached in memory
// for a short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn: PropTypes.func.isRequired,
clientSecret: PropTypes.string,
sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired,
@ -55,6 +64,7 @@ module.exports = createReactClass({
// registration shouldn't know or care how login is done.
onLoginClick: PropTypes.func.isRequired,
onServerConfigChange: PropTypes.func.isRequired,
defaultDeviceDisplayName: PropTypes.string,
},
getInitialState: function() {
@ -110,12 +120,13 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._replaceClient();
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -224,29 +235,56 @@ module.exports = createReactClass({
serverRequiresIdServer,
busy: false,
});
const showGenericError = (e) => {
this.setState({
errorText: _t("Unable to query for supported registration methods."),
// add empty flows array to get rid of spinner
flows: [],
});
};
try {
await this._makeRegisterRequest({});
// This should never succeed since we specified an empty
// auth object.
console.log("Expecting 401 from register request but got success!");
// We do the first registration request ourselves to discover whether we need to
// do SSO instead. If we've already started the UI Auth process though, we don't
// need to.
if (!this.state.doingUIAuth) {
await this._makeRegisterRequest({});
// This should never succeed since we specified an empty
// auth object.
console.log("Expecting 401 from register request but got success!");
}
} catch (e) {
if (e.httpStatus === 401) {
this.setState({
flows: e.data.flows,
});
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
this.setState({
errorText: _t("Registration has been disabled on this homeserver."),
// add empty flows array to get rid of spinner
flows: [],
});
// At this point registration is pretty much disabled, but before we do that let's
// quickly check to see if the server supports SSO instead. If it does, we'll send
// the user off to the login page to figure their account out.
try {
const loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used
});
const flows = await loginLogic.getFlows();
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
if (hasSsoFlow) {
// Redirect to login page - server probably expects SSO only
dis.dispatch({action: 'start_login'});
} else {
this.setState({
serverErrorIsFatal: true, // fatal because user cannot continue on this server
errorText: _t("Registration has been disabled on this homeserver."),
// add empty flows array to get rid of spinner
flows: [],
});
}
} catch (e) {
console.error("Failed to get login flows to check for SSO support", e);
showGenericError(e);
}
} else {
console.log("Unable to query for supported registration methods.", e);
this.setState({
errorText: _t("Unable to query for supported registration methods."),
// add empty flows array to get rid of spinner
flows: [],
});
showGenericError(e);
}
}
},
@ -347,7 +385,7 @@ module.exports = createReactClass({
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
});
}, this.state.formVals.password);
this._setupPushers(cli);
// we're still busy until we get unmounted: don't show the registration form again
@ -425,15 +463,14 @@ module.exports = createReactClass({
// session).
if (!this.state.formVals.password) inhibitLogin = null;
return this.state.matrixClient.register(
this.state.formVals.username,
this.state.formVals.password,
undefined, // session id: included in the auth dict already
auth,
null,
null,
inhibitLogin,
);
const registerParams = {
username: this.state.formVals.username,
password: this.state.formVals.password,
initial_device_display_name: this.props.defaultDeviceDisplayName,
};
if (auth) registerParams.auth = auth;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
return this.state.matrixClient.registerRequest(registerParams);
},
_getUIAuthInputs: function() {
@ -576,7 +613,6 @@ module.exports = createReactClass({
render: function() {
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
const AuthPage = sdk.getComponent('auth.AuthPage');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let errorText;

View file

@ -0,0 +1,200 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {
phase: store.phase,
// this serves dual purpose as the object for the request logic and
// the presence of it indicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
if (store.phase === PHASE_FINISHED) {
this.props.onFinished();
return;
}
this.setState({
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
});
};
componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
_onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
}
onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
}
onSkipBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterSkip();
}
onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
phase,
} = this.state;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
return (
<div>
<p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " +
"granting it access to encrypted messages.",
)}</p>
<p>{_t(
"This requires the latest Riot on your other devices:",
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>Riot Web</div>
<div>Riot Desktop</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>Riot iOS</div>
<div>Riot X for Android</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{_t("Use Recovery Passphrase or Key")}
</AccessibleButton>
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_DONE) {
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
return (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified" />
{message}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="primary"
onClick={this.onDoneClick}
>
{_t("Done")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_CONFIRM_SKIP) {
return (
<div>
<p>{_t(
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
className="warning"
kind="secondary"
onClick={this.onSkipConfirmClick}
>
{_t("Skip")}
</AccessibleButton>
<AccessibleButton
kind="danger"
onClick={this.onSkipBackClick}
>
{_t("Go Back")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
}
}
}

View file

@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {sendLoginRequest} from "../../../Login";
import url from 'url';
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
const LOGIN_VIEW = {
LOADING: 1,
@ -53,8 +54,7 @@ export default class SoftLogout extends React.Component {
this.state = {
loginView: LOGIN_VIEW.LOADING,
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
ssoUrl: null,
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
busy: false,
password: "",
@ -65,7 +65,7 @@ export default class SoftLogout extends React.Component {
componentDidMount(): void {
// We've ended up here when we don't need to - navigate to login
if (!Lifecycle.isSoftLogout()) {
dis.dispatch({action: "on_logged_in"});
dis.dispatch({action: "start_login"});
return;
}
@ -82,7 +82,7 @@ export default class SoftLogout extends React.Component {
onFinished: (wipeData) => {
if (!wipeData) return;
console.log("Clearing data from soft-logged-out device");
console.log("Clearing data from soft-logged-out session");
Lifecycle.logout();
},
});
@ -104,18 +104,6 @@ export default class SoftLogout extends React.Component {
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
this.setState({loginView: chosenView});
if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) {
const client = MatrixClientPeg.get();
const appUrl = url.parse(window.location.href, true);
appUrl.hash = ""; // Clear #/soft_logout off the URL
appUrl.query["homeserver"] = client.getHomeserverUrl();
appUrl.query["identityServer"] = client.getIdentityServerUrl();
const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso");
this.setState({ssoUrl});
}
}
onPasswordChange = (ev) => {
@ -194,14 +182,6 @@ export default class SoftLogout extends React.Component {
});
}
onSsoLogin = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
window.location.href = this.state.ssoUrl;
};
_renderSignInSection() {
if (this.state.loginView === LOGIN_VIEW.LOADING) {
const Spinner = sdk.getComponent("elements.Spinner");
@ -211,8 +191,8 @@ export default class SoftLogout extends React.Component {
let introText = null; // null is translated to something area specific in this function
if (this.state.keyBackupNeeded) {
introText = _t(
"Regain access to your account and recover encryption keys stored on this device. " +
"Without them, you wont be able to read all of your secure messages on any device.");
"Regain access to your account and recover encryption keys stored in this session. " +
"Without them, you wont be able to read all of your secure messages in any session.");
}
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
@ -233,7 +213,6 @@ export default class SoftLogout extends React.Component {
<p>{introText}</p>
{error}
<Field
id="softlogout_password"
type="password"
label={_t("Password")}
onChange={this.onPasswordChange}
@ -256,8 +235,6 @@ export default class SoftLogout extends React.Component {
}
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
if (!introText) {
introText = _t("Sign in and regain access to your account.");
} // else we already have a message and should use it (key backup warning)
@ -265,9 +242,11 @@ export default class SoftLogout extends React.Component {
return (
<div>
<p>{introText}</p>
<AccessibleButton kind='primary' onClick={this.onSsoLogin}>
{_t('Sign in with single sign-on')}
</AccessibleButton>
<SSOButton
matrixClient={MatrixClientPeg.get()}
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
</div>
);
}
@ -284,7 +263,6 @@ export default class SoftLogout extends React.Component {
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -306,7 +284,7 @@ export default class SoftLogout extends React.Component {
<p>
{_t(
"Warning: Your personal data (including encryption keys) is still stored " +
"on this device. Clear it if you're finished using this device, or want to sign " +
"in this session. Clear it if you're finished using this session, or want to sign " +
"in to another account.",
)}
</p>

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,13 +20,13 @@ import { _t } from '../../../languageHandler';
import React from 'react';
import createReactClass from 'create-react-class';
module.exports = createReactClass({
export default createReactClass({
displayName: 'AuthFooter',
render: function() {
return (
<div className="mx_AuthFooter">
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
</div>
);
},

View file

@ -16,12 +16,17 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import * as sdk from '../../../index';
module.exports = createReactClass({
export default createReactClass({
displayName: 'AuthHeader',
propTypes: {
disableLanguageSelector: PropTypes.bool,
},
render: function() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -29,7 +34,7 @@ module.exports = createReactClass({
return (
<div className="mx_AuthHeader">
<AuthHeaderLogo />
<LanguageSelector />
<LanguageSelector disabled={this.props.disableLanguageSelector} />
</div>
);
},

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,22 +17,21 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
module.exports = createReactClass({
displayName: 'AuthPage',
render: function() {
@replaceableComponent("views.auth.AuthPage")
export default class AuthPage extends React.PureComponent {
render() {
const AuthFooter = sdk.getComponent('auth.AuthFooter');
return (
<div className="mx_AuthPage">
<div className="mx_AuthPage_modal">
{ this.props.children }
{this.props.children}
</div>
<AuthFooter />
</div>
);
},
});
}
}

View file

@ -24,7 +24,7 @@ const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
module.exports = createReactClass({
export default createReactClass({
displayName: 'CaptchaForm',
propTypes: {
@ -46,7 +46,8 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
@ -61,13 +62,9 @@ module.exports = createReactClass({
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
let protocol = global.location.protocol;
if (protocol === "vector:") {
protocol = "https:";
}
const scriptTag = document.createElement('script');
scriptTag.setAttribute(
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
);
this._recaptchaContainer.current.appendChild(scriptTag);
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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';
export default class CompleteSecurityBody extends React.PureComponent {
render() {
return <div className="mx_CompleteSecurityBody">
{ this.props.children }
</div>;
}
}

View file

@ -17,9 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
@ -60,7 +60,7 @@ export default class CountryDropdown extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
if (!this.props.value) {
// If no value is given, we start with the default
// country selected, but our parent component
@ -80,7 +80,7 @@ export default class CountryDropdown extends React.Component {
}
_flagImgForIso2(iso2) {
return <img src={require(`../../../../res/img/flags/${iso2}.png`)} />;
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
}
_getShortOption(iso2) {

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = createReactClass({
export default createReactClass({
displayName: 'CustomServerDialog',
render: function() {

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,9 +22,10 @@ import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
* onPhaseChange: A function which is called when the stage's phase changes. If
* the stage has no phases, call this with DEFAULT_PHASE. Takes
* one argument, the phase, and is always defined/required.
* continueText: For stages which have a continue button, the text to use.
* continueKind: For stages which have a continue button, the style of button to
* use. For example, 'danger' or 'primary'.
* onCancel A function with no arguments which is called by the stage if the
* user knowingly cancelled/dismissed the authentication attempt.
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
export const DEFAULT_PHASE = 0;
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@ -142,10 +158,9 @@ export const PasswordAuthEntry = createReactClass({
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field
id="mx_InteractiveAuthEntryComponents_password"
className={passwordBoxClass}
type="password"
name="passwordField"
@ -176,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
_onCaptchaResponse: function(response) {
@ -237,8 +257,14 @@ export const TermsAuthEntry = createReactClass({
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
// TODO: [REACT-WARNING] Move this to constructor
componentWillMount: function() {
// example stageParams:
//
@ -331,7 +357,7 @@ export const TermsAuthEntry = createReactClass({
checkboxes.push(
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
</label>,
);
}
@ -379,16 +405,21 @@ export const EmailIdentityAuthEntry = createReactClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
requestingToken: false,
};
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
render: function() {
if (this.state.requestingToken) {
// This component is now only displayed once the token has been requested,
// so we know the email has been sent. It can also get loaded after the user
// has clicked the validation link if the server takes a while to propagate
// the validation internally. If we're in the session spawned from clicking
// the validation link, we won't know the email address, so if we don't have it,
// assume that the link has been clicked and the server will realise when we poll.
if (this.props.inputs.emailAddress === undefined) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
} else {
@ -421,6 +452,7 @@ export const MsisdnAuthEntry = createReactClass({
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -430,7 +462,9 @@ export const MsisdnAuthEntry = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
@ -565,6 +599,91 @@ export const MsisdnAuthEntry = createReactClass({
},
});
export class SSOAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
continueText: PropTypes.string,
continueKind: PropTypes.string,
onCancel: PropTypes.func,
};
static LOGIN_TYPE = "m.login.sso";
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
static PHASE_PREAUTH = 1; // button to start SSO
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
_ssoUrl: string;
constructor(props) {
super(props);
// We actually send the user through fallback auth so we don't have to
// deal with a redirect back to us, losing application context.
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId,
);
this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH,
};
}
componentDidMount(): void {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
}
onStartAuthClick = () => {
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
// certainly will need to open the thing in a new tab to avoid losing application
// context.
window.open(this._ssoUrl, '_blank');
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};
onConfirmClick = () => {
this.props.submitAuthDict({});
};
render() {
let continueButton = null;
const cancelButton = (
<AccessibleButton
onClick={this.props.onCancel}
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
>{_t("Cancel")}</AccessibleButton>
);
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
continueButton = (
<AccessibleButton
onClick={this.onStartAuthClick}
kind={this.props.continueKind || 'primary'}
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
);
} else {
continueButton = (
<AccessibleButton
onClick={this.onConfirmClick}
kind={this.props.continueKind || 'primary'}
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
);
}
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
{cancelButton}
{continueButton}
</div>;
}
}
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
@ -574,9 +693,15 @@ export const FallbackAuthEntry = createReactClass({
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
},
componentWillMount: function() {
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
@ -598,12 +723,16 @@ export const FallbackAuthEntry = createReactClass({
}
},
_onShowFallbackClick: function() {
_onShowFallbackClick: function(e) {
e.preventDefault();
e.stopPropagation();
const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId,
);
this._popupWindow = window.open(url);
this._popupWindow.opener = null;
},
_onReceiveMessage: function(event) {
@ -626,7 +755,7 @@ export const FallbackAuthEntry = createReactClass({
}
return (
<div>
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);
@ -639,11 +768,12 @@ const AuthEntryComponents = [
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
SSOAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {
export default function getEntryComponentForLoginType(loginType) {
for (const c of AuthEntryComponents) {
if (c.LOGIN_TYPE == loginType) {
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
return c;
}
}

View file

@ -18,7 +18,7 @@ import SdkConfig from "../../../SdkConfig";
import {getCurrentLanguage} from "../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import PlatformPeg from "../../../PlatformPeg";
import sdk from '../../../index';
import * as sdk from '../../../index';
import React from 'react';
function onChange(newLang) {
@ -28,12 +28,14 @@ function onChange(newLang) {
}
}
export default function LanguageSelector() {
export default function LanguageSelector({disabled}) {
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <LanguageDropdown className="mx_AuthBody_language"
return <LanguageDropdown
className="mx_AuthBody_language"
onOptionChange={onChange}
value={getCurrentLanguage()}
disabled={disabled}
/>;
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
@ -99,14 +99,15 @@ export default class ModularServerConfig extends ServerConfig {
"Enter the location of your Modular homeserver. It may use your own " +
"domain name or be a subdomain of <a>modular.im</a>.",
{}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}
</a>,
},
)}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
<Field
id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}

View file

@ -0,0 +1,125 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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, {PureComponent, RefCallback, RefObject} from "react";
import classNames from "classnames";
import zxcvbn from "zxcvbn";
import SdkConfig from "../../../SdkConfig";
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
import {_t, _td} from "../../../languageHandler";
import Field from "../elements/Field";
interface IProps {
autoFocus?: boolean;
id?: string;
className?: string;
minScore: 0 | 1 | 2 | 3 | 4;
value: string;
fieldRef?: RefCallback<Field> | RefObject<Field>;
label?: string;
labelEnterPassword?: string;
labelStrongPassword?: string;
labelAllowedButUnsafe?: string;
onChange(ev: React.FormEvent<HTMLElement>);
onValidate(result: IValidationResult);
}
interface IState {
complexity: zxcvbn.ZXCVBNResult;
}
class PassphraseField extends PureComponent<IProps, IState> {
static defaultProps = {
label: _td("Password"),
labelEnterPassword: _td("Enter password"),
labelStrongPassword: _td("Nice, strong password!"),
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
};
state = { complexity: null };
public readonly validate = withValidation<this>({
description: function() {
const complexity = this.state.complexity;
const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t(this.props.labelEnterPassword),
},
{
key: "complexity",
test: async function({ value }) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
this.setState({ complexity });
const safe = complexity.score >= this.props.minScore;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (this.state.complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword);
}
return _t(this.props.labelAllowedButUnsafe);
},
invalid: function() {
const complexity = this.state.complexity;
if (!complexity) {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
},
},
],
});
onValidate = async (fieldState: IFieldState) => {
const result = await this.validate(fieldState);
this.props.onValidate(result);
return result;
};
render() {
return <Field
id={this.props.id}
autoFocus={this.props.autoFocus}
className={classNames("mx_PassphraseField", this.props.className)}
ref={this.props.fieldRef}
type="password"
autoComplete="new-password"
label={_t(this.props.label)}
value={this.props.value}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>
}
}
export default PassphraseField;

View file

@ -19,10 +19,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton";
/**
* A pure UI component which displays a username/password form.
@ -44,6 +45,7 @@ export default class PasswordLogin extends React.Component {
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
busy: PropTypes.bool,
};
static defaultProps = {
@ -183,7 +185,7 @@ export default class PasswordLogin extends React.Component {
this.props.onPasswordChanged(ev.target.value);
}
renderLoginField(loginType) {
renderLoginField(loginType, autoFocus) {
const Field = sdk.getComponent('elements.Field');
const classes = {};
@ -193,7 +195,6 @@ export default class PasswordLogin extends React.Component {
classes.error = this.props.loginIncorrect && !this.state.username;
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_email"
name="username" // make it a little easier for browser's remember-password
key="email_input"
type="text"
@ -202,13 +203,13 @@ export default class PasswordLogin extends React.Component {
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
autoFocus
disabled={this.props.disableSubmit}
autoFocus={autoFocus}
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.error = this.props.loginIncorrect && !this.state.username;
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_username"
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
@ -216,7 +217,8 @@ export default class PasswordLogin extends React.Component {
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
autoFocus
disabled={this.props.disableSubmit}
autoFocus={autoFocus}
/>;
case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
@ -231,7 +233,6 @@ export default class PasswordLogin extends React.Component {
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_phoneNumber"
name="phoneNumber"
key="phone_input"
type="text"
@ -240,7 +241,8 @@ export default class PasswordLogin extends React.Component {
prefix={phoneCountry}
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
autoFocus
disabled={this.props.disableSubmit}
autoFocus={autoFocus}
/>;
}
}
@ -265,12 +267,16 @@ export default class PasswordLogin extends React.Component {
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = <span>
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
a: sub => <a className="mx_Login_forgot"
onClick={this.onForgotPasswordClick}
href="#"
>
{sub}
</a>,
a: sub => (
<AccessibleButton
className="mx_Login_forgot"
disabled={this.props.busy}
kind="link"
onClick={this.onForgotPasswordClick}
>
{sub}
</AccessibleButton>
),
})}
</span>;
}
@ -279,7 +285,10 @@ export default class PasswordLogin extends React.Component {
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
const loginField = this.renderLoginField(this.state.loginType);
// If login is empty, autoFocus login, otherwise autoFocus password.
// this is for when auto server discovery remounts us when the user tries to tab from username to password
const autoFocusPassword = !this.isLoginEmpty();
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
let loginType;
if (!SdkConfig.get().disable_3pid_login) {
@ -287,10 +296,10 @@ export default class PasswordLogin extends React.Component {
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Field
id="mx_PasswordLogin_type"
element="select"
value={this.state.loginType}
onChange={this.onLoginTypeChange}
disabled={this.props.disableSubmit}
>
<option
key={PasswordLogin.LOGIN_FIELD_MXID}
@ -324,19 +333,20 @@ export default class PasswordLogin extends React.Component {
{loginField}
<Field
className={pwFieldClass}
id="mx_PasswordLogin_password"
type="password"
name="password"
label={_t('Password')}
value={this.state.password}
onChange={this.onPasswordChanged}
disabled={this.props.disableSubmit}
autoFocus={autoFocusPassword}
/>
{forgotPasswordJsx}
<input className="mx_Login_submit"
{ !this.props.busy && <input className="mx_Login_submit"
type="submit"
value={_t('Sign in')}
disabled={this.props.disableSubmit}
/>
/> }
</form>
</div>
);

View file

@ -20,8 +20,8 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Email from '../../../email';
import * as sdk from '../../../index';
import * as Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import PassphraseField from "./PassphraseField";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -41,7 +42,7 @@ const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from of
/**
* A pure UI component which displays a registration form.
*/
module.exports = createReactClass({
export default createReactClass({
displayName: 'RegistrationForm',
propTypes: {
@ -76,9 +77,8 @@ module.exports = createReactClass({
email: this.props.defaultEmail || "",
phoneNumber: this.props.defaultPhoneNumber || "",
password: this.props.defaultPassword || "",
passwordConfirm: "",
passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
passwordSafe: false,
};
},
@ -102,11 +102,15 @@ module.exports = createReactClass({
"No identity server is configured so you cannot add an email address in order to " +
"reset your password in the future.",
);
} else {
} else if (this._showEmail()) {
desc = _t(
"If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?",
);
} else {
// user can't set an e-mail so don't prompt them to
self._doSubmit(ev);
return;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -260,65 +264,10 @@ module.exports = createReactClass({
});
},
async onPasswordValidate(fieldState) {
const result = await this.validatePasswordRules(fieldState);
onPasswordValidate(result) {
this.markFieldValid(FIELD_PASSWORD, result.valid);
return result;
},
validatePasswordRules: withValidation({
description: function() {
const complexity = this.state.passwordComplexity;
const score = complexity ? complexity.score : 0;
return <progress
className="mx_AuthBody_passwordScore"
max={PASSWORD_MIN_SCORE}
value={score}
/>;
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter password"),
},
{
key: "complexity",
test: async function({ value }) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
const safe = complexity.score >= PASSWORD_MIN_SCORE;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
passwordSafe: safe,
});
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (!this.state.passwordSafe) {
return _t("Password is allowed, but unsafe");
}
return _t("Nice, strong password!");
},
invalid: function() {
const complexity = this.state.passwordComplexity;
if (!complexity) {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
},
},
],
}),
onPasswordConfirmChange(ev) {
this.setState({
passwordConfirm: ev.target.value,
@ -470,7 +419,6 @@ module.exports = createReactClass({
_t("Email") :
_t("Email (optional)");
return <Field
id="mx_RegistrationForm_email"
ref={field => this[FIELD_EMAIL] = field}
type="text"
label={emailPlaceholder}
@ -481,12 +429,10 @@ module.exports = createReactClass({
},
renderPassword() {
const Field = sdk.getComponent('elements.Field');
return <Field
return <PassphraseField
id="mx_RegistrationForm_password"
ref={field => this[FIELD_PASSWORD] = field}
type="password"
label={_t("Password")}
fieldRef={field => this[FIELD_PASSWORD] = field}
minScore={PASSWORD_MIN_SCORE}
value={this.state.password}
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
@ -499,6 +445,7 @@ module.exports = createReactClass({
id="mx_RegistrationForm_passwordConfirm"
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password"
autoComplete="new-password"
label={_t("Confirm")}
value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange}
@ -522,7 +469,6 @@ module.exports = createReactClass({
onOptionChange={this.onPhoneCountryChange}
/>;
return <Field
id="mx_RegistrationForm_phoneNumber"
ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text"
label={phoneLabel}

View file

@ -19,12 +19,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import { createClient } from 'matrix-js-sdk/lib/matrix';
import { createClient } from 'matrix-js-sdk/src/matrix';
import classNames from 'classnames';
/*
@ -72,7 +72,8 @@ export default class ServerConfig extends React.PureComponent {
};
}
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
@ -223,7 +224,8 @@ export default class ServerConfig extends React.PureComponent {
{sub}
</a>,
})}
<Field id="mx_ServerConfig_hsUrl"
<Field
id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
@ -246,7 +248,7 @@ export default class ServerConfig extends React.PureComponent {
{sub}
</a>,
})}
<Field id="mx_ServerConfig_isUrl"
<Field
label={_t("Identity Server URL")}
placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl || ''}
@ -274,15 +276,13 @@ export default class ServerConfig extends React.PureComponent {
: null;
return (
<div className="mx_ServerConfig">
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
<h3>{_t("Other servers")}</h3>
{errorText}
{this._renderHomeserverSection()}
{this._renderIdentityServerSection()}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
{submitButton}
</form>
</div>
{submitButton}
</form>
);
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import * as sdk from '../../../index';
import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
@ -46,7 +46,7 @@ export const TYPES = {
label: () => _t('Premium'),
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}
</a>,
}),

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import * as sdk from "../../../index";
import PropTypes from "prop-types";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";

View file

@ -15,12 +15,18 @@ limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import * as Matrix from "matrix-js-sdk";
import {_td} from "../../../languageHandler";
import PlatformPeg from "../../../PlatformPeg";
// translatable strings for Welcome pages
_td("Sign in with SSO");
export default class Welcome extends React.PureComponent {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
@ -33,11 +39,25 @@ export default class Welcome extends React.PureComponent {
pageUrl = 'welcome.html';
}
const {hsUrl, isUrl} = this.props.serverConfig;
const tmpClient = Matrix.createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
const plaf = PlatformPeg.get();
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
this.props.fragmentAfterLogin);
return (
<AuthPage>
<div className="mx_Welcome">
<EmbeddedPage className="mx_WelcomePage"
<EmbeddedPage
className="mx_WelcomePage"
url={pageUrl}
replaceMap={{
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
}}
/>
<LanguageSelector />
</div>

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,195 +17,182 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
module.exports = createReactClass({
displayName: 'BaseAvatar',
const useImageUrl = ({url, urls}) => {
const [imageUrls, setUrls] = useState([]);
const [urlsIndex, setIndex] = useState();
propTypes: {
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
},
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop',
defaultToInitialLetter: true,
};
},
getInitialState: function() {
return this._getState(this.props);
},
componentDidMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync);
},
componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
const newImageUrls = newState.imageUrls;
const oldImageUrls = this.state.imageUrls;
if (newImageUrls.length !== oldImageUrls.length) {
this.setState(newState); // detected a new entry
} else {
// check each one to see if they are the same
for (let i = 0; i < newImageUrls.length; i++) {
if (oldImageUrls[i] !== newImageUrls[i]) {
this.setState(newState); // detected a diff
break;
}
}
}
},
onClientSync: function(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected &&
// Did we fall back?
this.state.urlsIndex > 0
) {
// Start from the highest priority URL again
this.setState({
urlsIndex: 0,
});
}
},
_getState: function(props) {
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
// imageUrls: [ props.url, ...props.urls ]
let urls = [];
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || [];
_urls = memoizedUrls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
let defaultImageUrl = null;
if (props.defaultToInitialLetter) {
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
props.idName || props.name,
);
urls.push(defaultImageUrl); // lowest priority
}
// deduplicate URLs
urls = Array.from(new Set(urls));
_urls = Array.from(new Set(_urls));
return {
imageUrls: urls,
defaultImageUrl: defaultImageUrl,
urlsIndex: 0,
};
},
setIndex(0);
setUrls(_urls);
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
onError: function(ev) {
const nextIndex = this.state.urlsIndex + 1;
if (nextIndex < this.state.imageUrls.length) {
// try the next one
this.setState({
urlsIndex: nextIndex,
});
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
setIndex(0);
}
},
}, []);
useEventEmitter(cli, "sync", onClientSync);
render: function() {
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const imageUrl = imageUrls[urlsIndex];
return [imageUrl, onError];
};
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter, onClick,
...otherProps
} = this.props;
const BaseAvatar = (props) => {
const {
name,
idName,
title,
url,
urls,
width=40,
height=40,
resizeMethod="crop", // eslint-disable-line no-unused-vars
defaultToInitialLetter=true,
onClick,
inputRef,
...otherProps
} = props;
const [imageUrl, onError] = useImageUrl({url, urls});
if (!imageUrl && defaultToInitialLetter) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span
className="mx_BaseAvatar_initial"
aria-hidden="true"
style={{
fontSize: toPx(width * 0.65),
width: toPx(width),
lineHeight: toPx(height),
}}
>
{ initialLetter }
</span>
);
const imgNode = (
<img
className="mx_BaseAvatar_image"
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
alt=""
title={title}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
aria-hidden="true" />
);
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{ initialLetter }
</span>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} aria-hidden="true" />
);
if (onClick != null) {
return (
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps}
>
{ textNode }
{ imgNode }
</AccessibleButton>
);
} else {
return (
<span className="mx_BaseAvatar" {...otherProps}>
{ textNode }
{ imgNode }
</span>
);
}
}
if (onClick != null) {
return (
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
<AccessibleButton
{...otherProps}
element="span"
className="mx_BaseAvatar"
onClick={onClick}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
inputRef={inputRef}
>
{ textNode }
{ imgNode }
</AccessibleButton>
);
} else {
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
{ textNode }
{ imgNode }
</span>
);
}
},
});
}
if (onClick != null) {
return (
<AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
onClick={onClick}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
title={title} alt=""
inputRef={inputRef}
{...otherProps} />
);
} else {
return (
<img
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
title={title} alt=""
ref={inputRef}
{...otherProps} />
);
}
};
BaseAvatar.displayName = "BaseAvatar";
BaseAvatar.propTypes = {
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
onClick: PropTypes.func,
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
};
export default BaseAvatar;

View file

@ -17,8 +17,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'GroupAvatar',

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,11 +18,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const Avatar = require('../../../Avatar');
const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
module.exports = createReactClass({
export default createReactClass({
displayName: 'MemberAvatar',
propTypes: {
@ -32,7 +34,7 @@ module.exports = createReactClass({
resizeMethod: PropTypes.string,
// The onClick to give the avatar
onClick: PropTypes.func,
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
viewUserOnClick: PropTypes.bool,
title: PropTypes.string,
},
@ -50,19 +52,24 @@ module.exports = createReactClass({
return this._getState(this.props);
},
componentWillReceiveProps: function(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
this.setState(this._getState(nextProps));
},
_getState: function(props) {
if (props.member) {
if (props.member && props.member.name) {
return {
name: props.member.name,
title: props.title || props.member.userId,
imageUrl: Avatar.avatarUrlForMember(props.member,
props.width,
props.height,
props.resizeMethod),
imageUrl: props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
};
} else if (props.fallbackUserId) {
return {
@ -82,8 +89,8 @@ module.exports = createReactClass({
if (viewUserOnClick) {
onClick = () => {
dispatcher.dispatch({
action: 'view_user',
dis.dispatch({
action: Action.ViewUser,
member: this.props.member,
});
};

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {_t} from "../../../languageHandler";
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component {
resizeMethod: 'crop',
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
hasStatus: this.hasStatus,
@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
this._button = createRef();
}
componentWillMount() {
componentDidMount() {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
@ -63,7 +63,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
componentWillUmount() {
componentWillUnmount() {
const { user } = this.props.member;
if (!user) {
return;

View file

@ -16,13 +16,13 @@ limitations under the License.
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import sdk from "../../../index";
import Avatar from '../../../Avatar';
import * as sdk from "../../../index";
import * as Avatar from '../../../Avatar';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomAvatar',
// Room may be left unset here, but if it is,
@ -63,7 +63,8 @@ module.exports = createReactClass({
}
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps),
});
@ -82,7 +83,7 @@ module.exports = createReactClass({
getImageUrls: function(props) {
return [
ContentRepo.getHttpUriForMxc(
getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl,
Math.floor(props.width * window.devicePixelRatio),

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk';
@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component {
onFinished: PropTypes.func,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._onClickReject = this._onClickReject.bind(this);
}

View file

@ -22,9 +22,9 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {EventStatus} from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import Resend from '../../../Resend';
@ -37,7 +37,7 @@ function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
module.exports = createReactClass({
export default createReactClass({
displayName: 'MessageContextMenu',
propTypes: {
@ -61,7 +61,7 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
this._checkPermissions();
},
@ -90,7 +90,8 @@ module.exports = createReactClass({
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
if (!pinnedEvent) return false;
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
const content = pinnedEvent.getContent();
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
},
onResendClick: function() {
@ -115,11 +116,6 @@ module.exports = createReactClass({
this.closeMenu();
},
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
@ -129,22 +125,24 @@ module.exports = createReactClass({
},
onViewSourceClick: function() {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
content: this.props.mxEvent.event,
roomId: ev.getRoomId(),
eventId: ev.getId(),
content: ev.event,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
onViewClearSourceClick: function() {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
roomId: ev.getRoomId(),
eventId: ev.getId(),
// FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent,
content: ev._clearEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
@ -414,15 +412,20 @@ module.exports = createReactClass({
}
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
<MenuItem className="mx_MessageContextMenu_field">
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</a>
<MenuItem
element="a"
className="mx_MessageContextMenu_field"
onClick={this.onPermalinkClick}
href={permalink}
target="_blank"
rel="noreferrer noopener"
>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</MenuItem>
);
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
if (this.props.eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
{ _t('Quote') }
@ -436,16 +439,15 @@ module.exports = createReactClass({
isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
<MenuItem className="mx_MessageContextMenu_field">
<a
href={mxEvent.event.content.external_url}
target="_blank"
rel="noopener"
onClick={this.closeMenu}
tabIndex={-1}
>
{ _t('Source URL') }
</a>
<MenuItem
element="a"
className="mx_MessageContextMenu_field"
target="_blank"
rel="noreferrer noopener"
onClick={this.closeMenu}
href={mxEvent.event.content.external_url}
>
{ _t('Source URL') }
</MenuItem>
);
}
@ -458,15 +460,6 @@ module.exports = createReactClass({
);
}
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</MenuItem>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
@ -493,7 +486,6 @@ module.exports = createReactClass({
{ quoteButton }
{ externalURLButton }
{ collapseReplyThread }
{ e2eInfo }
{ reportEventButton }
</div>
);

View file

@ -21,10 +21,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';
@ -63,7 +63,7 @@ const NotifOption = ({active, onClick, src, label}) => {
);
};
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {
@ -82,7 +82,7 @@ module.exports = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
},
@ -306,7 +306,7 @@ module.exports = createReactClass({
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" alt="" />
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/feather-customised/settings.svg")} width="15" height="15" alt="" />
{ _t('Settings') }
</MenuItem>
</div>

View file

@ -17,8 +17,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
export default class StatusMessageContextMenu extends React.Component {
@ -27,15 +27,15 @@ export default class StatusMessageContextMenu extends React.Component {
user: PropTypes.object,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
message: this.comittedStatusMessage,
};
}
componentWillMount() {
componentDidMount() {
const { user } = this.props;
if (!user) {
return;

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import sdk from '../../../index';
import * as sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default class TagTileContextMenu extends React.Component {
static propTypes = {
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
onFinished: PropTypes.func.isRequired,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static contextType = MatrixClientContext;
constructor() {
super();
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
}
_onRemoveClick() {
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished();
}

View file

@ -17,16 +17,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export class TopLeftMenu extends React.Component {
export default class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
@ -46,15 +49,7 @@ export class TopLeftMenu extends React.Component {
}
hasHomePage() {
const config = SdkConfig.get();
const pagesConfig = config.embeddedPages;
if (pagesConfig && pagesConfig.homeUrl) {
return true;
}
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
return !!config.welcomePageUrl;
return !!getHomePageUrl(SdkConfig.get());
}
render() {
@ -67,10 +62,11 @@ export class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
a: sub =>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
@ -100,6 +96,12 @@ export class TopLeftMenu extends React.Component {
);
}
const helpItem = (
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
{_t("Help")}
</MenuItem>
);
const settingsItem = (
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
@ -115,18 +117,25 @@ export class TopLeftMenu extends React.Component {
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{helpItem}
{signInOutItem}
</ul>
</div>;
}
openHelp = () => {
this.closeMenu();
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
};
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() {
dis.dispatch({action: 'view_user_settings'});
dis.fire(Action.ViewUserSettings);
this.closeMenu();
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,7 +20,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = createReactClass({
export default createReactClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: PropTypes.func,

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,7 +26,7 @@ const Presets = {
Custom: "custom",
};
module.exports = createReactClass({
export default createReactClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: PropTypes.func,

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,7 +20,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = createReactClass({
export default createReactClass({
displayName: 'RoomAlias',
propTypes: {
// Specifying a homeserver will make magical things happen when you,
@ -97,7 +98,7 @@ module.exports = createReactClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);

View file

@ -22,9 +22,9 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -44,7 +45,7 @@ const addressTypeName = {
};
module.exports = createReactClass({
export default createReactClass({
displayName: "AddressPickerDialog",
propTypes: {
@ -107,6 +108,7 @@ module.exports = createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
@ -614,7 +616,7 @@ module.exports = createReactClass({
onManageSettingsClick(e) {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
dis.fire(Action.ViewUserSettings);
this.onCancel();
},

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
@ -72,7 +72,7 @@ export default createReactClass({
<button onClick={this._onInviteNeverWarnClicked}>
{ _t('Invite anyway and never warn me again') }
</button>
<button onClick={this._onInviteClicked} autoFocus="true">
<button onClick={this._onInviteClicked} autoFocus={true}>
{ _t('Invite anyway') }
</button>
</div>

View file

@ -22,12 +22,11 @@ import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import { Key } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
/**
* Basic container for modal dialogs.
@ -66,6 +65,9 @@ export default createReactClass({
// Title for the dialog.
title: PropTypes.node.isRequired,
// Path to an icon to put in the header
headerImage: PropTypes.string,
// children should be the content of the dialog
children: PropTypes.node,
@ -84,17 +86,8 @@ export default createReactClass({
};
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
};
},
componentWillMount() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
@ -121,37 +114,47 @@ export default createReactClass({
);
}
let headerImage;
if (this.props.headerImage) {
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage}
alt=""
/>;
}
return (
<FocusLock
returnFocus={true}
lockProps={{
onKeyDown: this._onKeyDown,
role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
["aria-describedby"]: this.props.contentId,
}}
className={classNames({
[this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth,
})}
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{ this.props.title }
<MatrixClientContext.Provider value={this._matrixClient}>
<FocusLock
returnFocus={true}
lockProps={{
onKeyDown: this._onKeyDown,
role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
["aria-describedby"]: this.props.contentId,
}}
className={classNames({
[this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth,
})}
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{headerImage}
{ this.props.title }
</div>
{ this.props.headerButton }
{ cancelButton }
</div>
{ this.props.headerButton }
{ cancelButton }
</div>
{ this.props.children }
</FocusLock>
{ this.props.children }
</FocusLock>
</MatrixClientContext.Provider>
);
},
});

View file

@ -19,14 +19,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport from '../../../rageshake/submit-rageshake';
export default class BugReportDialog extends React.Component {
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
sendLogs: true,
busy: false,
@ -67,32 +68,30 @@ export default class BugReportDialog extends React.Component {
this.setState({ busy: true, progress: null, err: null });
this._sendProgressCallback(_t("Preparing to send logs"));
require(['../../../rageshake/submit-rageshake'], (s) => {
s(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
title: _t('Logs sent'),
description: _t('Thank you!'),
hasCancelButton: false,
});
}
}, (err) => {
if (!this._unmounted) {
this.setState({
busy: false,
progress: null,
err: _t("Failed to send logs: ") + `${err.message}`,
});
}
});
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
title: _t('Logs sent'),
description: _t('Thank you!'),
hasCancelButton: false,
});
}
}, (err) => {
if (!this._unmounted) {
this.setState({
busy: false,
progress: null,
err: _t("Failed to send logs: ") + `${err.message}`,
});
}
});
}
@ -138,12 +137,20 @@ export default class BugReportDialog extends React.Component {
);
}
let warning;
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
warning = <p><b>
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
</b></p>;
}
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ warning }
<p>
{ _t(
"Debug logs contain application usage data including your " +
@ -167,7 +174,6 @@ export default class BugReportDialog extends React.Component {
) }
</b></p>
<Field
id="mx_BugReportDialog_issueUrl"
type="text"
className="mx_BugReportDialog_field_input"
label={_t("GitHub issue")}
@ -176,7 +182,6 @@ export default class BugReportDialog extends React.Component {
placeholder="https://github.com/vector-im/riot-web/issues/..."
/>
<Field
id="mx_BugReportDialog_notes"
className="mx_BugReportDialog_field_input"
element="textarea"
label={_t("Notes")}

View file

@ -17,7 +17,7 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import request from 'browser-request';
import { _t } from '../../../languageHandler';
@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
_elementsForCommit(commit) {
return (
<li key={commit.sha} className="mx_ChangelogDialog_li">
<a href={commit.html_url} target="_blank" rel="noopener">
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
{commit.commit.message.split('\n')[0]}
</a>
</li>

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*

View file

@ -0,0 +1,65 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
this.props.onFinished(true);
};
_onDecline = () => {
this.props.onFinished(false);
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog
className='mx_ConfirmDestroyCrossSigningDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Destroy cross-signing keys?")}>
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
<p>
{_t(
"Deleting cross-signing keys is permanent. " +
"Anyone you have verified with will see security alerts. " +
"You almost certainly don't want to do this, unless " +
"you've lost every device you can cross-sign from.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("Clear cross-signing keys")}
onPrimaryButtonClick={this._onConfirm}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this._onDecline}
/>
</BaseDialog>
);
}
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*

View file

@ -18,7 +18,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
@ -55,7 +55,8 @@ export default createReactClass({
askReason: false,
}),
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._reasonField = null;
},

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import * as sdk from "../../../index";
export default class ConfirmWipeDeviceDialog extends React.Component {
static propTypes = {
@ -39,11 +39,11 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
return (
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Clear all data on this device?")}>
title={_t("Clear all data in this session?")}>
<div className='mx_ConfirmWipeDeviceDialog_content'>
<p>
{_t(
"Clearing all data from this device is permanent. Encrypted messages will be lost " +
"Clearing all data from this session is permanent. Encrypted messages will be lost " +
"unless their keys have been backed up.",
)}
</p>

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'CreateGroupDialog',

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,23 +18,26 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
defaultPublic: PropTypes.bool,
},
getInitialState() {
const config = SdkConfig.get();
return {
isPublic: false,
isPublic: this.props.defaultPublic || false,
isEncrypted: true,
name: "",
topic: "",
alias: "",
@ -44,13 +48,13 @@ export default createReactClass({
},
_roomCreateOptions() {
const createOpts = {};
const opts = {};
const createOpts = opts.createOpts = {};
createOpts.name = this.state.name;
if (this.state.isPublic) {
createOpts.visibility = "public";
createOpts.preset = "public_chat";
// to prevent createRoom from enabling guest access
createOpts['initial_state'] = [];
opts.guestAccess = false;
const {alias} = this.state;
const localPart = alias.substr(1, alias.indexOf(":") - 1);
createOpts['room_alias_name'] = localPart;
@ -61,7 +65,12 @@ export default createReactClass({
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
return createOpts;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts;
},
componentDidMount() {
@ -126,6 +135,10 @@ export default createReactClass({
this.setState({isPublic});
},
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) {
this.setState({alias});
},
@ -165,19 +178,31 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
e2eeSection = <React.Fragment>
<LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")}
onChange={this.onEncryptedChange}
value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
/>
<p>{ _t("You cant disable this later. Bridges & most bots wont work yet.") }</p>
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
@ -187,11 +212,11 @@ export default createReactClass({
>
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
@ -42,11 +42,9 @@ export default (props) => {
};
const description =
_t("You've previously used a newer version of Riot on %(host)s. " +
_t("You've previously used a newer version of Riot with this session. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ",
{host: props.host},
);
"need to sign out and back in again.");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,110 +18,165 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) {
super(props, context);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
constructor(props) {
super(props);
this.state = {
password: "",
busy: false,
shouldErase: false,
errStr: null,
authData: null, // for UIA
authEnabled: true, // see usages for information
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: null,
continueText: null,
continueKind: null,
};
this._initAuth(/* shouldErase= */false);
}
_onPasswordFieldChange(ev) {
this.setState({
password: ev.target.value,
});
}
_onStagePhaseChange = (stage, phase) => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "danger",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
continueText: _t("Confirm account deactivation"),
continueKind: "danger",
},
};
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
}
async _onOk() {
this.setState({busy: true});
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: MatrixClientPeg.get().credentials.userId,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
const DEACTIVATE_AESTHETICS = {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[PasswordAuthEntry.LOGIN_TYPE]: {
[DEFAULT_PHASE]: {
body: _t("To continue, please enter your password:"),
},
password: this.state.password,
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
}
this.setState({
busy: false,
errStr: errStr,
});
},
};
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
let continueKind = null;
if (aesthetics) {
const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
this.setState({bodyText, continueText, continueKind});
};
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return;
}
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
_onUIAuthComplete = (auth) => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
});
};
_onEraseFieldChange = (ev) => {
this.setState({
shouldErase: ev.target.checked,
// Disable the auth form because we're going to have to reinitialize the auth
// information. We do this because we can't modify the parameters in the UIA
// session, and the user will have selected something which changes the request.
// Therefore, we throw away the last auth session and try a new one.
authEnabled: false,
});
// As mentioned above, set up for auth again to get updated UIA session info
this._initAuth(/* shouldErase= */ev.target.checked);
};
_onCancel() {
this.props.onFinished(false);
}
_initAuth(shouldErase) {
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data, authEnabled: true});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
});
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null;
if (this.state.errStr) {
error = <div className="error">
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{ _t("Cancel") }
</button>;
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData && this.state.authEnabled) {
auth = (
<div>
{this.state.bodyText}
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
</div>
);
}
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}
>
@ -171,29 +226,10 @@ export default class DeactivateAccountDialog extends React.Component {
</label>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<Field
id="mx_DeactivateAccountDialog_password"
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
{error}
{auth}
</div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -19,25 +19,27 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import DMRoomMap from '../../../utils/DMRoomMap';
import createRoom from "../../../createRoom";
import dis from "../../../dispatcher";
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_SHOW_SAS = 2;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
const PHASE_VERIFIED = 4;
const PHASE_CANCELLED = 5;
const PHASE_PICK_VERIFICATION_OPTION = 2;
const PHASE_SHOW_SAS = 3;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
const PHASE_VERIFIED = 5;
const PHASE_CANCELLED = 6;
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
@ -50,6 +52,7 @@ export default class DeviceVerifyDialog extends React.Component {
super();
this._verifier = null;
this._showSasEvent = null;
this._request = null;
this.state = {
phase: PHASE_START,
mode: MODE_SAS,
@ -81,6 +84,25 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(false);
}
_onUseSasClick = async () => {
try {
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
this._request = null;
}
};
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
@ -97,17 +119,33 @@ export default class DeviceVerifyDialog extends React.Component {
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
this._verifier = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
const request = await client.requestVerificationDM(
this.props.userId, roomId,
);
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,
verificationMethods.RECIPROCATE_QR_CODE,
]);
await this._request.waitFor(r => r.ready || r.started);
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
if (!this._verifier) return;
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
@ -145,10 +183,13 @@ export default class DeviceVerifyDialog extends React.Component {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderSasVerificationPhaseStart();
body = this._renderVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderSasVerificationPhaseWaitAccept();
body = this._renderVerificationPhaseWaitAccept();
break;
case PHASE_PICK_VERIFICATION_OPTION:
body = this._renderVerificationPhasePick();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
@ -157,17 +198,17 @@ export default class DeviceVerifyDialog extends React.Component {
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderSasVerificationPhaseVerified();
body = this._renderVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderSasVerificationPhaseCancelled();
body = this._renderVerificationPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify device")}
title={_t("Verify session")}
onFinished={this._onCancelClick}
>
{body}
@ -175,7 +216,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseStart() {
_renderVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
@ -189,10 +230,7 @@ export default class DeviceVerifyDialog extends React.Component {
{ _t("Verify by comparing a short text string.") }
</p>
<p>
{_t(
"For maximum security, we recommend you do this in person or " +
"use another trusted means of communication.",
)}
{_t("To be secure, do this in person or use a trusted way to communicate.")}
</p>
<DialogButtons
primaryButton={_t('Begin Verifying')}
@ -204,7 +242,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseWaitAccept() {
_renderVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
@ -225,12 +263,23 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderVerificationPhasePick() {
return <VerificationQREmojiOptions
request={this._request}
onCancel={this._onCancelClick}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
inDialog={true}
/>;
}
@ -244,12 +293,12 @@ export default class DeviceVerifyDialog extends React.Component {
</div>;
}
_renderSasVerificationPhaseVerified() {
_renderVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderSasVerificationPhaseCancelled() {
_renderVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}
@ -260,12 +309,12 @@ export default class DeviceVerifyDialog extends React.Component {
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("To verify that this device can be trusted, please check that the key you see " +
text = _t("To verify that this session can be trusted, please check that the key you see " +
"in User Settings on that device matches the key below:");
} else {
text = _t("To verify that this device can be trusted, please contact its owner using some other " +
text = _t("To verify that this session can be trusted, please contact its owner using some other " +
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
"for this device matches the key below:");
"for this session matches the key below:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
@ -281,14 +330,14 @@ export default class DeviceVerifyDialog extends React.Component {
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"If it doesn't, then someone else is intercepting this session " +
"and you probably want to press the blacklist button instead.") }
</p>
</div>
@ -296,7 +345,7 @@ export default class DeviceVerifyDialog extends React.Component {
return (
<QuestionDialog
title={_t("Verify device")}
title={_t("Verify session")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
@ -316,23 +365,7 @@ export default class DeviceVerifyDialog extends React.Component {
}
async function ensureDMExistsAndOpen(userId) {
const client = MatrixClientPeg.get();
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({

View file

@ -14,25 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { Room } from "matrix-js-sdk";
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
class DevtoolsComponent extends React.Component {
static contextTypes = {
roomId: PropTypes.string.isRequired,
};
}
import {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
class GenericEditor extends DevtoolsComponent {
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
this.onBack = this.onBack.bind(this);
}
@ -67,12 +72,15 @@ class SendCustomEvent extends GenericEditor {
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
forceStateEvent: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({
@ -91,11 +99,11 @@ class SendCustomEvent extends GenericEditor {
}
send(content) {
const cli = MatrixClientPeg.get();
const cli = this.context;
if (this.state.isStateEvent) {
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
} else {
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
return cli.sendEvent(this.props.room.roomId, this.state.eventType, content);
}
}
@ -154,13 +162,16 @@ class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); }
static propTypes = {
room: PropTypes.instanceOf(Room).isRequired,
isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({
@ -177,9 +188,9 @@ class SendAccountData extends GenericEditor {
}
send(content) {
const cli = MatrixClientPeg.get();
const cli = this.context;
if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
}
return cli.setAccountData(this.state.eventType, content);
}
@ -234,7 +245,7 @@ class SendAccountData extends GenericEditor {
const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50;
class FilteredList extends React.Component {
class FilteredList extends React.PureComponent {
static propTypes = {
children: PropTypes.any,
query: PropTypes.string,
@ -247,8 +258,8 @@ class FilteredList extends React.Component {
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
}
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
@ -256,7 +267,8 @@ class FilteredList extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
this.setState({
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
@ -291,7 +303,7 @@ class FilteredList extends React.Component {
render() {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return <div>
<Field id="DevtoolsDialog_FilteredList_filter" label={_t('Filter results')} autoFocus={true} size={64}
<Field label={_t('Filter results')} autoFocus={true} size={64}
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
// force re-render so that autoFocus is applied when this component is re-used
@ -305,19 +317,20 @@ class FilteredList extends React.Component {
}
}
class RoomStateExplorer extends DevtoolsComponent {
class RoomStateExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Room State'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
this.roomStateEvents = room.currentState.events;
constructor(props) {
super(props);
this.roomStateEvents = this.props.room.currentState.events;
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
@ -373,7 +386,7 @@ class RoomStateExplorer extends DevtoolsComponent {
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
@ -442,15 +455,18 @@ class RoomStateExplorer extends DevtoolsComponent {
}
}
class AccountDataExplorer extends DevtoolsComponent {
class AccountDataExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Account Data'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
@ -467,11 +483,10 @@ class AccountDataExplorer extends DevtoolsComponent {
}
getData() {
const cli = MatrixClientPeg.get();
if (this.state.isRoomAccountData) {
return cli.getRoom(this.context.roomId).accountData;
return this.props.room.accountData;
}
return cli.store.accountData;
return this.context.store.accountData;
}
onViewSourceClick(event) {
@ -505,10 +520,14 @@ class AccountDataExplorer extends DevtoolsComponent {
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
return <SendAccountData
room={this.props.room}
isRoomAccountData={this.state.isRoomAccountData}
onBack={this.onBack}
inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
}
return <div className="mx_ViewSource">
@ -553,17 +572,20 @@ class AccountDataExplorer extends DevtoolsComponent {
}
}
class ServersInRoomList extends DevtoolsComponent {
class ServersInRoomList extends React.PureComponent {
static getLabel() { return _t('View Servers in Room'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
constructor(props, context) {
super(props, context);
static contextType = MatrixClientContext;
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
constructor(props) {
super(props);
const room = this.props.room;
const servers = new Set();
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
this.servers = Array.from(servers).map(s =>
@ -594,27 +616,110 @@ class ServersInRoomList extends DevtoolsComponent {
}
}
const PHASE_MAP = {
[PHASE_UNSENT]: "unsent",
[PHASE_REQUESTED]: "requested",
[PHASE_READY]: "ready",
[PHASE_DONE]: "done",
[PHASE_STARTED]: "started",
[PHASE_CANCELLED]: "cancelled",
};
function VerificationRequest({txnId, request}) {
const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout);
/* Re-render if something changes state */
useEventEmitter(request, "change", updateState);
/* Keep re-rendering if there's a timeout */
useEffect(() => {
if (request.timeout == 0) return;
/* Note that request.timeout is a getter, so its value changes */
const id = setInterval(() => {
setRequestTimeout(request.timeout);
}, 500);
return () => { clearInterval(id); };
}, [request]);
return (<div className="mx_DevTools_VerificationRequest">
<dl>
<dt>Transaction</dt>
<dd>{txnId}</dd>
<dt>Phase</dt>
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
<dt>Timeout</dt>
<dd>{Math.floor(timeout / 1000)}</dd>
<dt>Methods</dt>
<dd>{request.methods && request.methods.join(", ")}</dd>
<dt>requestingUserId</dt>
<dd>{request.requestingUserId}</dd>
<dt>observeOnly</dt>
<dd>{JSON.stringify(request.observeOnly)}</dd>
</dl>
</div>);
}
class VerificationExplorer extends React.Component {
static getLabel() {
return _t("Verification Requests");
}
/* Ensure this.context is the cli */
static contextType = MatrixClientContext;
onNewRequest = () => {
this.forceUpdate();
}
componentDidMount() {
const cli = this.context;
cli.on("crypto.verification.request", this.onNewRequest);
}
componentWillUnmount() {
const cli = this.context;
cli.off("crypto.verification.request", this.onNewRequest);
}
render() {
const cli = this.context;
const room = this.props.room;
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (<div>
<div className="mx_Dialog_content">
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
)}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onBack}>{_t("Back")}</button>
</div>
</div>);
}
}
const Entries = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
AccountDataExplorer,
ServersInRoomList,
VerificationExplorer,
];
export default class DevtoolsDialog extends React.Component {
static childContextTypes = {
roomId: PropTypes.string.isRequired,
// client: PropTypes.instanceOf(MatixClient),
};
export default class DevtoolsDialog extends React.PureComponent {
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this);
this.onCancel = this.onCancel.bind(this);
@ -627,10 +732,6 @@ export default class DevtoolsDialog extends React.Component {
this._unmounted = true;
}
getChildContext() {
return { roomId: this.props.roomId };
}
_setMode(mode) {
return () => {
this.setState({ mode });
@ -654,15 +755,17 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body = <div>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} />
</div>;
body = <MatrixClientContext.Consumer>
{(cli) => <React.Fragment>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
</React.Fragment>}
</MatrixClientContext.Consumer>;
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
body = <React.Fragment>
<div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
@ -679,7 +782,7 @@ export default class DevtoolsDialog extends React.Component {
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
</div>
</div>;
</React.Fragment>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -28,7 +28,7 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default createReactClass({
@ -42,6 +42,7 @@ export default createReactClass({
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
},
getDefaultProps: function() {
@ -56,9 +57,12 @@ export default createReactClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}
contentId='mx_Dialog_content'
<BaseDialog
className="mx_ErrorDialog"
onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}
headerImage={this.props.headerImage}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description || _t('An error has occurred.') }

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
const PHASE_START = 0;
@ -121,6 +121,8 @@ export default class IncomingSasDialog extends React.Component {
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
let profile;
if (this.state.opponentProfile) {
profile = <div className="mx_IncomingSasDialog_opponentProfile">
@ -148,20 +150,36 @@ export default class IncomingSasDialog extends React.Component {
profile = <Spinner />;
}
const userDetailText = [
<p key="p1">{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>,
<p key="p2">{_t(
// NB. Below wording adjusted to singular 'session' until we have
// cross-signing
"Verifying this user will mark their session as trusted, and " +
"also mark your session as trusted to them.",
)}</p>,
];
const selfDetailText = [
<p key="p1">{_t(
"Verify this device to mark it as trusted. " +
"Trusting this device gives you and other users extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>,
<p key="p2">{_t(
"Verifying this device will mark it as trusted, and users who have verified with " +
"you will trust this device.",
)}</p>,
];
return (
<div>
{profile}
<p>{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>
<p>{_t(
// NB. Below wording adjusted to singular 'device' until we have
// cross-signing
"Verifying this user will mark their device as trusted, and " +
"also mark your device as trusted to them.",
)}</p>
{isSelf ? selfDetailText : userDetailText}
<DialogButtons
primaryButton={_t('Continue')}
hasCancel={true}
@ -178,6 +196,8 @@ export default class IncomingSasDialog extends React.Component {
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
inDialog={true}
/>;
}
@ -227,6 +247,7 @@ export default class IncomingSasDialog extends React.Component {
<BaseDialog
title={_t("Incoming Verification Request")}
onFinished={this._onFinished}
fixedWidth={false}
>
{body}
</BaseDialog>

View file

@ -19,7 +19,7 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classNames from "classnames";
@ -32,6 +32,7 @@ export default createReactClass({
button: PropTypes.string,
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,
},
getDefaultProps: function() {
@ -50,10 +51,13 @@ export default createReactClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
<BaseDialog
className="mx_InfoDialog"
onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
hasCancel={this.props.hasCloseButton}
onKeyDown={this.props.onKeyDown}
>
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
{ this.props.description }

View file

@ -17,8 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import dis from '../../../dispatcher';
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import {Action} from "../../../dispatcher/actions";
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
_onOpenSettingsClick = () => {
this.props.onFinished();
dis.dispatch({action: "view_user_settings"});
dis.fire(Action.ViewUserSettings);
};
render() {

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import * as sdk from "../../../index";
export default class IntegrationsImpossibleDialog extends React.Component {
static propTypes = {

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,10 +20,12 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
export default createReactClass({
displayName: 'InteractiveAuthDialog',
@ -44,12 +47,60 @@ export default createReactClass({
onFinished: PropTypes.func.isRequired,
// Optional title and body to show when not showing a particular stage
title: PropTypes.string,
body: PropTypes.string,
// Optional title and body pairs for particular stages and phases within
// those stages. Object structure/example is:
// {
// "org.example.stage_type": {
// 1: {
// "body": "This is a body for phase 1" of org.example.stage_type,
// "title": "Title for phase 1 of org.example.stage_type"
// },
// 2: {
// "body": "This is a body for phase 2 of org.example.stage_type",
// "title": "Title for phase 2 of org.example.stage_type"
// "continueText": "Confirm identity with Example Auth",
// "continueKind": "danger"
// }
// }
// }
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases: PropTypes.object,
},
getInitialState: function() {
return {
authError: null,
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
},
_getDefaultDialogAesthetics: function() {
const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("To continue, use Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm to continue"),
body: _t("Click the button below to confirm your identity."),
continueText: _t("Confirm"),
continueKind: "primary",
},
};
return {
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
};
},
@ -57,12 +108,21 @@ export default createReactClass({
if (success) {
this.props.onFinished(true, result);
} else {
this.setState({
authError: result,
});
if (result === ERROR_USER_CANCELLED) {
this.props.onFinished(false, null);
} else {
this.setState({
authError: result,
});
}
}
},
_onUpdateStagePhase: function(newStage, newPhase) {
// We copy the stage and stage phase params into state for title selection in render()
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
},
_onDismissClick: function() {
this.props.onFinished(false);
},
@ -71,6 +131,24 @@ export default createReactClass({
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Let's pick a title, body, and other params text that we'll show to the user. The order
// is most specific first, so stagePhase > our props > defaults.
let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication'));
let body = this.state.authError ? null : this.props.body;
let continueText = null;
let continueKind = null;
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
if (aesthetics && aesthetics.title) title = aesthetics.title;
if (aesthetics && aesthetics.body) body = aesthetics.body;
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
}
}
let content;
if (this.state.authError) {
content = (
@ -88,11 +166,16 @@ export default createReactClass({
} else {
content = (
<div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth}
{body}
<InteractiveAuth
ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished}
onStagePhaseChange={this._onUpdateStagePhase}
continueText={continueText}
continueKind={continueKind}
/>
</div>
);
@ -101,7 +184,7 @@ export default createReactClass({
return (
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
title={title}
contentId='mx_Dialog_content'
>
{ content }

File diff suppressed because it is too large Load diff

View file

@ -1,175 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{ _t('Ignore request') }
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading device info...') }</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
},
});

View file

@ -0,0 +1,108 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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, {useState, useCallback, useRef} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default function KeySignatureUploadFailedDialog({
failures,
source,
continuation,
onFinished,
}) {
const RETRIES = 2;
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner');
const [retry, setRetry] = useState(RETRIES);
const [cancelled, setCancelled] = useState(false);
const [retrying, setRetrying] = useState(false);
const [success, setSuccess] = useState(false);
const onCancel = useRef(onFinished);
const causes = new Map([
["_afterCrossSigningLocalKeyChange", _t("a new master key signature")],
["checkOwnCrossSigningTrust", _t("a new cross-signing key signature")],
["setDeviceVerification", _t("a device cross-signing signature")],
]);
const defaultCause = _t("a key signature");
const onRetry = useCallback(async () => {
try {
setRetrying(true);
const cancel = new Promise((resolve, reject) => {
onCancel.current = reject;
}).finally(() => {
setCancelled(true);
});
await Promise.race([
continuation(),
cancel,
]);
setSuccess(true);
} catch (e) {
setRetry(r => r-1);
} finally {
onCancel.current = onFinished;
setRetrying(false);
}
}, [continuation, onFinished]);
let body;
if (!success && !cancelled && continuation && retry > 0) {
const reason = causes.get(source) || defaultCause;
body = (<div>
<p>{_t("Riot encountered an error during upload of:")}</p>
<p>{reason}</p>
{retrying && <Spinner />}
<pre>{JSON.stringify(failures, null, 2)}</pre>
<DialogButtons
primaryButton='Retry'
hasCancel={true}
onPrimaryButtonClick={onRetry}
onCancel={onCancel.current}
primaryDisabled={retrying}
/>
</div>);
} else {
body = (<div>
{success ?
<span>{_t("Upload completed")}</span> :
cancelled ?
<span>{_t("Cancelled signature upload")}</span> :
<span>{_t("Unable to upload")}</span>}
<DialogButtons
primaryButton={_t("OK")}
hasCancel={false}
onPrimaryButtonClick={onFinished}
/>
</div>);
}
return (
<BaseDialog
title={success ?
_t("Signature upload success") :
_t("Signature upload failed")}
fixedWidth={false}
onFinished={() => {}}
>
{body}
</BaseDialog>
);
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,11 +17,10 @@ limitations under the License.
import React from 'react';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default class LogoutDialog extends React.Component {
defaultProps = {
@ -35,8 +35,8 @@ export default class LogoutDialog extends React.Component {
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
const cli = MatrixClientPeg.get();
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
this.state = {
shouldLoadBackupStatus: shouldLoadBackupStatus,
@ -94,10 +94,14 @@ export default class LogoutDialog extends React.Component {
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}
@ -133,7 +137,7 @@ export default class LogoutDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let setupButtonCaption;
if (this.state.backupInfo) {
setupButtonCaption = _t("Connect this device to Key Backup");
setupButtonCaption = _t("Connect this session to Key Backup");
} else {
// if there's an error fetching the backup info, we'll just assume there's
// no backup for the purpose of the button caption

View file

@ -0,0 +1,86 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
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 {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
export default class ManualDeviceKeyVerificationDialog extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
_onCancelClick = () => {
this.props.onFinished(false);
}
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
}
render() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:");
} else {
text = _t("Confirm this user's session by comparing the following with their User Settings:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<p>
{ text }
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If they don't match, the security of your communication may be compromised.") }
</p>
</div>
);
return (
<QuestionDialog
title={_t("Verify session")}
description={body}
button={_t("Verify session")}
onFinished={this._onLegacyFinished}
/>
);
}
}

View file

@ -16,9 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import sdk from "../../../index";
import * as sdk from "../../../index";
import {wantsDateSeparator} from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';

Some files were not shown because too many files have changed in this diff Show more