Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/snapshot

This commit is contained in:
Richard Lewis 2018-04-03 11:34:14 +01:00
commit f8f8bc469e
85 changed files with 3694 additions and 513 deletions

View file

@ -31,7 +31,6 @@ import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GeminiScrollbar from 'react-gemini-scrollbar';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
const LONG_DESC_PLACEHOLDER = _td(
@ -671,6 +670,20 @@ export default React.createClass({
});
},
_onJoinClick: function() {
this.setState({membershipBusy: true});
this._matrixClient.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to join community"),
});
});
},
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
@ -687,9 +700,9 @@ export default React.createClass({
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to leave room"),
description: _t("Unable to leave community"),
});
});
},
@ -707,8 +720,21 @@ export default React.createClass({
});
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ?
<div className="mx_GroupView_changeDelayWarning">
{ _t(
'Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> ' +
'might not be seen by other users for up to 30 minutes.',
{},
{
'bold1': (sub) => <b> { sub } </b>,
'bold2': (sub) => <b> { sub } </b>,
},
) }
</div> : <div />;
return <div className={groupSettingsSectionClasses}>
{ header }
{ changeDelayWarning }
{ this._getLongDescriptionNode() }
{ this._getRoomsNode() }
</div>;
@ -847,9 +873,8 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const group = this._matrixClient.getGroup(this.props.groupId);
if (!group) return null;
if (group.myMembership === 'invite') {
if (group && group.myMembership === 'invite') {
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
return <div className="mx_GroupView_membershipSection">
<Spinner />
@ -890,33 +915,72 @@ export default React.createClass({
</div>
</div>
</div>;
} else if (group.myMembership === 'join' && this.state.editing) {
const leaveButtonTooltip = this.state.isUserPrivileged ?
}
let membershipContainerExtraClasses;
let membershipButtonExtraClasses;
let membershipButtonTooltip;
let membershipButtonText;
let membershipButtonOnClick;
// User is not in the group
if ((!group || group.myMembership === 'leave') &&
this.state.summary &&
this.state.summary.profile &&
Boolean(this.state.summary.profile.is_joinable)
) {
membershipButtonText = _t("Join this community");
membershipButtonOnClick = this._onJoinClick;
membershipButtonExtraClasses = 'mx_GroupView_joinButton';
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_leave';
} else if (
group &&
group.myMembership === 'join' &&
this.state.editing
) {
membershipButtonText = _t("Leave this community");
membershipButtonOnClick = this._onLeaveClick;
membershipButtonTooltip = this.state.isUserPrivileged ?
_t("You are an administrator of this community") :
_t("You are a member of this community");
const leaveButtonClasses = classnames({
"mx_RoomHeader_textButton": true,
"mx_GroupView_textButton": true,
"mx_GroupView_leaveButton": true,
"mx_RoomHeader_textButton_danger": this.state.isUserPrivileged,
});
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
<div className="mx_GroupView_membershipSubSection">
{ /* Empty div for flex alignment */ }
<div />
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton
className={leaveButtonClasses}
onClick={this._onLeaveClick}
title={leaveButtonTooltip}
>
{ _t("Leave") }
</AccessibleButton>
</div>
</div>
</div>;
membershipButtonExtraClasses = {
'mx_GroupView_leaveButton': true,
'mx_RoomHeader_textButton_danger': this.state.isUserPrivileged,
};
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_joined';
} else {
return null;
}
return null;
const membershipButtonClasses = classnames([
'mx_RoomHeader_textButton',
'mx_GroupView_textButton',
],
membershipButtonExtraClasses,
);
const membershipContainerClasses = classnames(
'mx_GroupView_membershipSection',
membershipContainerExtraClasses,
);
return <div className={membershipContainerClasses}>
<div className="mx_GroupView_membershipSubSection">
{ /* Empty div for flex alignment */ }
<div />
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton
className={membershipButtonClasses}
onClick={membershipButtonOnClick}
title={membershipButtonTooltip}
>
{ membershipButtonText }
</AccessibleButton>
</div>
</div>
</div>;
},
_getLongDescriptionNode: function() {
@ -962,6 +1026,7 @@ export default React.createClass({
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Spinner = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
return <Spinner />;
@ -1112,9 +1177,9 @@ export default React.createClass({
{ rightButtons }
</div>
</div>
<GeminiScrollbar className="mx_GroupView_body">
<GeminiScrollbarWrapper className="mx_GroupView_body">
{ bodyNodes }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
} else if (this.state.error) {

View file

@ -374,7 +374,7 @@ const LoggedInView = React.createClass({
}
return (
<div className='mx_MatrixChat_wrapper'>
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
{ topBar }
<DragDropContext onDragEnd={this._onDragEnd}>
<div className={bodyClasses}>

View file

@ -171,6 +171,10 @@ export default React.createClass({
register_hs_url: null,
register_is_url: null,
register_id_sid: null,
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: false,
};
return s;
},
@ -287,6 +291,8 @@ export default React.createClass({
this.handleResize();
window.addEventListener('resize', this.handleResize);
this._pageChanging = false;
// check we have the right tint applied for this theme.
// N.B. we don't call the whole of setTheme() here as we may be
// racing with the theme CSS download finishing from index.js
@ -364,13 +370,58 @@ export default React.createClass({
window.removeEventListener('resize', this.handleResize);
},
componentDidUpdate: function() {
componentWillUpdate: function(props, state) {
if (this.shouldTrackPageChange(this.state, state)) {
this.startPageChangeTimer();
}
},
componentDidUpdate: function(prevProps, prevState) {
if (this.shouldTrackPageChange(prevState, this.state)) {
const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'});
this.focusComposer = false;
}
},
startPageChangeTimer() {
// This shouldn't happen because componentWillUpdate and componentDidUpdate
// are used.
if (this._pageChanging) {
console.warn('MatrixChat.startPageChangeTimer: timer already started');
return;
}
this._pageChanging = true;
performance.mark('riot_MatrixChat_page_change_start');
},
stopPageChangeTimer() {
if (!this._pageChanging) {
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
return;
}
this._pageChanging = false;
performance.mark('riot_MatrixChat_page_change_stop');
performance.measure(
'riot_MatrixChat_page_change_delta',
'riot_MatrixChat_page_change_start',
'riot_MatrixChat_page_change_stop',
);
performance.clearMarks('riot_MatrixChat_page_change_start');
performance.clearMarks('riot_MatrixChat_page_change_stop');
const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop();
return measurement.duration;
},
shouldTrackPageChange(prevState, state) {
return prevState.currentRoomId !== state.currentRoomId ||
prevState.view !== state.view ||
prevState.page_type !== state.page_type;
},
setStateForNewView: function(state) {
if (state.view === undefined) {
throw new Error("setStateForNewView with no view!");
@ -608,6 +659,16 @@ export default React.createClass({
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
case 'aria_hide_main_app':
this.setState({
hideToSRUsers: true,
});
break;
case 'aria_unhide_main_app':
this.setState({
hideToSRUsers: false,
});
break;
}
},
@ -1171,18 +1232,6 @@ export default React.createClass({
cli.on("crypto.warning", (type) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
switch (type) {
case 'CRYPTO_WARNING_ACCOUNT_MIGRATED':
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Cryptography data migrated'),
description: _t(
"A one-off migration of cryptography data has been performed. "+
"End-to-end encryption will not work if you go back to an older "+
"version of Riot. If you need to use end-to-end cryptography on "+
"an older version, log out of Riot first. To retain message history, "+
"export and re-import your keys.",
),
});
break;
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Old cryptography data detected'),
@ -1339,7 +1388,6 @@ export default React.createClass({
if (this.props.onNewScreen) {
this.props.onNewScreen(screen);
}
Analytics.trackPageChange();
},
onAliasClick: function(event, alias) {

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../index';
import { _t } from '../../languageHandler';
import dis from '../../dispatcher';
@ -63,6 +62,8 @@ export default withMatrixClient(React.createClass({
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const GroupTile = sdk.getComponent("groups.GroupTile");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
let content;
let contentHeader;
@ -73,7 +74,7 @@ export default withMatrixClient(React.createClass({
});
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
content = groupNodes.length > 0 ?
<GeminiScrollbar>
<GeminiScrollbarWrapper>
<div className="mx_MyGroups_microcopy">
<p>
{ _t(
@ -92,7 +93,7 @@ export default withMatrixClient(React.createClass({
<div className="mx_MyGroups_joinedGroups">
{ groupNodes }
</div>
</GeminiScrollbar> :
</GeminiScrollbarWrapper> :
<div className="mx_MyGroups_placeholder">
{ _t(
"You're not currently a member of any communities.",

View file

@ -17,9 +17,9 @@ limitations under the License.
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird';
import { KeyCode } from '../../Keyboard';
import sdk from '../../index.js';
const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true;
@ -224,7 +224,7 @@ module.exports = React.createClass({
onResize: function() {
this.props.onResize();
this.checkScroll();
this.refs.geminiPanel.forceUpdate();
if (this._gemScroll) this._gemScroll.forceUpdate();
},
// after an update to the contents of the panel, check that the scroll is
@ -665,14 +665,25 @@ module.exports = React.createClass({
throw new Error("ScrollPanel._getScrollNode called when unmounted");
}
return this.refs.geminiPanel.scrollbar.getViewElement();
if (!this._gemScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
}
return this._gemScroll.scrollbar.getViewElement();
},
_collectGeminiScroll: function(gemScroll) {
this._gemScroll = gemScroll;
},
render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
// TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway.
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
return (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
onScroll={this.onScroll} onResize={this.onResize}
className={this.props.className} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
@ -680,7 +691,7 @@ module.exports = React.createClass({
{ this.props.children }
</ol>
</div>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
);
},
});

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import GeminiScrollbar from 'react-gemini-scrollbar';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
@ -101,6 +100,9 @@ const TagPanel = React.createClass({
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -112,12 +114,10 @@ const TagPanel = React.createClass({
});
const clearButton = this.state.selectedTags.length > 0 ?
<img
src="img/icons-close.svg"
<TintableSvg src="img/icons-close.svg" width="24" height="24"
alt={_t("Clear filter")}
title={_t("Clear filter")}
width="24"
height="24" /> :
/> :
<div />;
return <div className="mx_TagPanel">
@ -125,7 +125,7 @@ const TagPanel = React.createClass({
{ clearButton }
</AccessibleButton>
<div className="mx_TagPanel_divider" />
<GeminiScrollbar
<GeminiScrollbarWrapper
className="mx_TagPanel_scroller"
autoshow={true}
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
@ -146,7 +146,7 @@ const TagPanel = React.createClass({
</div>
) }
</Droppable>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
<div className="mx_TagPanel_divider" />
<div className="mx_TagPanel_createGroupButton">
<GroupsButton tooltip={true} />

View file

@ -624,6 +624,7 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({
action: 'on_room_read',
roomId: this.props.timelineSet.room.roomId,
});
}
}

View file

@ -30,7 +30,6 @@ import Promise from 'bluebird';
const packageJson = require('../../../package.json');
const UserSettingsStore = require('../../UserSettingsStore');
const CallMediaHandler = require('../../CallMediaHandler');
const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email');
const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig');
@ -795,11 +794,18 @@ module.exports = React.createClass({
}
return (
<div>
<h3>{ _t("Bug Report") }</h3>
<h3>{ _t("Debug Logs Submission") }</h3>
<div className="mx_UserSettings_section">
<p>{ _t("Found a bug?") }</p>
<p>{
_t( "If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contian messages.",
)
}</p>
<button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>{ _t('Report it') }
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
</button>
</div>
</div>
@ -1111,6 +1117,7 @@ module.exports = React.createClass({
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const Notifications = sdk.getComponent("settings.Notifications");
const EditableText = sdk.getComponent('elements.EditableText');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
@ -1206,8 +1213,9 @@ module.exports = React.createClass({
onCancelClick={this.props.onClose}
/>
<GeminiScrollbar className="mx_UserSettings_body"
autoshow={true}>
<GeminiScrollbarWrapper
className="mx_UserSettings_body"
autoshow={true}>
<h3>{ _t("Profile") }</h3>
@ -1320,7 +1328,7 @@ module.exports = React.createClass({
{ this._renderDeactivateAccount() }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -82,7 +83,7 @@ module.exports = React.createClass({
}
},
onClientSync(syncState, prevState) {
onClientSync: function(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.

View file

@ -48,12 +48,33 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
}
},
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps),
});
},
onRoomStateEvents: function(ev) {
if (ev.getRoomId() !== this.props.room.roomId ||
ev.getType() !== 'm.room.avatar'
) return;
this.setState({
urls: this.getImageUrls(this.props),
});
},
getImageUrls: function(props) {
return [
ContentRepo.getHttpUriForMxc(

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
@ -37,9 +38,6 @@ export default React.createClass({
// onFinished callback to call when Escape is pressed
onFinished: PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: PropTypes.func,
// called when a key is pressed
onKeyDown: PropTypes.func,
@ -52,6 +50,10 @@ export default React.createClass({
// children should be the content of the dialog
children: PropTypes.node,
// Id of content element
// If provided, this is used to add a aria-describedby attribute
contentId: React.PropTypes.string,
},
childContextTypes: {
@ -76,12 +78,6 @@ export default React.createClass({
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
} else if (e.keyCode === KeyCode.ENTER) {
if (this.props.onEnterPressed) {
e.stopPropagation();
e.preventDefault();
this.props.onEnterPressed(e);
}
}
},
@ -93,17 +89,28 @@ export default React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<FocusTrap onKeyDown={this._onKeyDown}
className={this.props.className}
role="dialog"
aria-labelledby='mx_BaseDialog_title'
// This should point to a node describing the dialog.
// If we were about to completelly 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}
>
<AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<div className={'mx_Dialog_title ' + this.props.titleClass}>
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title }
</div>
{ this.props.children }
</div>
</FocusTrap>
);
},
});

View file

@ -59,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
);
tiles.push(
<RoomTile key={room.roomId} room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
@ -128,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
@ -146,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
profile = <div className="error" role="alert">
Unable to load profile information for { this.props.userId }
</div>;
} else {
@ -162,14 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<DialogButtons primaryButton={_t('Start Chatting')}
onPrimaryButtonClick={this.props.onNewDMClick} />
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
</div>;
}
@ -178,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={this.props.onFinished.bind(false)}
title={title}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>

View file

@ -114,10 +114,10 @@ export default React.createClass({
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div id="mx_Dialog_content" className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
{ avatar }
</div>

View file

@ -112,7 +112,7 @@ export default React.createClass({
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
createErrorNode = <div className="error">
createErrorNode = <div className="error" role="alert">
<div>{ _t('Something went wrong whilst creating your community') }</div>
<div>{ this.state.createError.message }</div>
</div>;
@ -120,7 +120,6 @@ export default React.createClass({
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Community')}
>
<form onSubmit={this._onFormSubmit}>

View file

@ -45,30 +45,31 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={_t('Create Room')}
>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
</details>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
</details>
</div>
</form>
<DialogButtons primaryButton={_t('Create Room')}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />

View file

@ -52,22 +52,18 @@ export default React.createClass({
};
},
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content">
title={this.props.title || _t('Error')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description || _t('An error has occurred.') }
</div>
<div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
</div>

View file

@ -73,11 +73,12 @@ export default React.createClass({
let content;
if (this.state.authError) {
content = (
<div>
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
<div id='mx_Dialog_content'>
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
<br />
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
autoFocus="true"
>
{ _t("Dismiss") }
</AccessibleButton>
@ -85,7 +86,7 @@ export default React.createClass({
);
} else {
content = (
<div>
<div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
@ -100,6 +101,7 @@ export default React.createClass({
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>

View file

@ -126,11 +126,11 @@ export default React.createClass({
text = _t(text, {displayName: displayName});
return (
<div>
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
@ -154,7 +154,7 @@ export default React.createClass({
content = this._renderContent();
} else {
content = (
<div>
<div id='mx_Dialog_content'>
<p>{ _t('Loading device info...') }</p>
<Spinner />
</div>
@ -165,6 +165,7 @@ export default React.createClass({
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>

View file

@ -60,10 +60,10 @@ export default React.createClass({
}
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description }
</div>
<DialogButtons primaryButton={this.props.button || _t('OK')}

View file

@ -30,6 +30,12 @@ export default React.createClass({
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
if (this.refs.bugreportLink) {
this.refs.bugreportLink.focus();
}
},
_sendBugReport: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
@ -50,16 +56,20 @@ export default React.createClass({
{ _t(
"Otherwise, <a>click here</a> to send a bug report.",
{},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
key="bugreport" href='#'>{ sub }</a> },
) }
</p>
);
}
const shouldFocusContinueButton =!(bugreport==true);
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}>
<div className="mx_Dialog_content">
title={_t('Unable to restore session')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{ _t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.") }</p>
@ -71,7 +81,7 @@ export default React.createClass({
{ bugreport }
</div>
<DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked}
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
onCancel={this.props.onFinished} />
</BaseDialog>
);

View file

@ -41,9 +41,6 @@ export default React.createClass({
};
},
componentDidMount: function() {
},
onEmailAddressChanged: function(value) {
this.setState({
emailAddress: value,
@ -131,6 +128,7 @@ export default React.createClass({
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input"
autoFocus="true"
placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={false}
@ -140,9 +138,10 @@ export default React.createClass({
<BaseDialog className="mx_SetEmailDialog"
onFinished={this.onCancelled}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<p>
<p id='mx_Dialog_content'>
{ _t('This will allow you to reset your password and receive notifications.') }
</p>
{ emailInput }

View file

@ -235,14 +235,14 @@ export default React.createClass({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
authErrorIndicator = <div className="error" role="alert">
{ this.state.authError }
</div>;
}
@ -254,8 +254,9 @@ export default React.createClass({
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title={_t('To get started, please pick a username!')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}

View file

@ -61,17 +61,18 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
</form>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />

View file

@ -146,6 +146,7 @@ export default React.createClass({
},
render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.props.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
@ -189,8 +190,9 @@ export default React.createClass({
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title={_t('Room contains unknown devices')}
contentId='mx_Dialog_content'
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
<h4>
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4>
@ -198,7 +200,7 @@ export default React.createClass({
{ _t("Unknown devices") }:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
</GeminiScrollbarWrapper>
<DialogButtons primaryButton={sendButtonLabel}
onPrimaryButtonClick={sendButtonOnClick}
onCancel={this._onDismissClicked} />

View file

@ -17,6 +17,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
* as a button. Identifies the element as a button, setting proper tab
@ -28,8 +30,34 @@ import PropTypes from 'prop-types';
export default function AccessibleButton(props) {
const {element, onClick, children, ...restProps} = props;
restProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
// It's because we are using html buttons at a few places e.g. inside dialogs
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";

View file

@ -0,0 +1,33 @@
/*
Copyright 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import GeminiScrollbar from 'react-gemini-scrollbar';
function GeminiScrollbarWrapper(props) {
// Enable forceGemini so that gemini is always enabled. This is
// to avoid future issues where a feature is implemented without
// doing QA on every OS/browser combination.
//
// By default GeminiScrollbar allows native scrollbars to be used
// on macOS. Use forceGemini to enable Gemini's non-native
// scrollbars on all OSs.
return <GeminiScrollbar ref={props.wrappedRef} forceGemini={true} {...props}>
{ props.children }
</GeminiScrollbar>;
}
export default GeminiScrollbarWrapper;

View file

@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
displayName: 'GroupMemberInfo',
@ -180,9 +179,10 @@ module.exports = React.createClass({
);
const EmojiText = sdk.getComponent('elements.EmojiText');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
@ -199,7 +199,7 @@ module.exports = React.createClass({
</div>
{ adminTools }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -18,7 +18,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -134,6 +133,7 @@ export default React.createClass({
},
render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.fetching || this.state.fetchingInvitedMembers) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_MemberList">
@ -162,10 +162,10 @@ export default React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
{ joined }
{ invited }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -22,7 +22,6 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
displayName: 'GroupRoomInfo',
@ -157,6 +156,7 @@ module.exports = React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberInfo">
@ -216,7 +216,7 @@ module.exports = React.createClass({
const avatar = <BaseAvatar name={groupRoomName} width={36} height={36} url={avatarUrl} />;
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
@ -233,7 +233,7 @@ module.exports = React.createClass({
</div>
{ adminTools }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -17,7 +17,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_ROOMS = 30;
@ -120,16 +119,17 @@ export default React.createClass({
</form>
);
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{ this.makeGroupRoomTiles(this.state.searchQuery) }
</TruncatedList>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -128,12 +128,22 @@ export const PasswordAuthEntry = React.createClass({
);
}
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t("Password:") }</p>
<form onSubmit={this._onSubmit}>
<label htmlFor="passwordField">{ _t("Password:") }</label>
<input
name="passwordField"
ref="passwordField"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
@ -143,9 +153,7 @@ export const PasswordAuthEntry = React.createClass({
{ submitButtonOrSpinner }
</div>
</form>
<div className="error">
{ this.props.errorText }
</div>
{ errorSection }
</div>
);
},
@ -180,14 +188,22 @@ export const RecaptchaAuthEntry = React.createClass({
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
const sitePublicKey = this.props.stageParams.public_key;
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse}
/>
<div className="error">
{ this.props.errorText }
</div>
{ errorSection }
</div>
);
},
@ -372,6 +388,14 @@ export const MsisdnAuthEntry = React.createClass({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_UserSettings_button: true, // XXX button classes
});
let errorSection;
if (this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.state.errorText }
</div>
);
}
return (
<div>
<p>{ _t("A text message has been sent to %(msisdn)s",
@ -385,6 +409,7 @@ export const MsisdnAuthEntry = React.createClass({
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token}
onChange={this._onTokenChange}
aria-label={ _t("Code")}
/>
<br />
<input type="submit" value={_t("Submit")}
@ -392,9 +417,7 @@ export const MsisdnAuthEntry = React.createClass({
disabled={!enableSubmit}
/>
</form>
<div className="error">
{ this.state.errorText }
</div>
{errorSection}
</div>
</div>
);
@ -427,6 +450,12 @@ export const FallbackAuthEntry = React.createClass({
}
},
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
}
},
_onShowFallbackClick: function() {
const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
@ -445,12 +474,18 @@ export const FallbackAuthEntry = React.createClass({
},
render: function() {
return (
<div>
<a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<div className="error">
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);
},

View file

@ -337,7 +337,7 @@ module.exports = React.createClass({
_addCodeCopyButton() {
// Add 'copy' buttons to pre blocks
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = (e) => {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -32,7 +33,11 @@ const PRESENCE_CLASS = {
};
function presenceClassForMember(presenceState, lastActiveAgo) {
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
if (showPresence === false) {
return 'mx_EntityTile_online_beenactive';
}
// offline is split into two categories depending on whether we have
// a last_active_ago for them.
if (presenceState == 'offline') {
@ -64,6 +69,7 @@ const EntityTile = React.createClass({
shouldComponentUpdate: PropTypes.func,
onClick: PropTypes.func,
suppressOnHover: PropTypes.bool,
showPresence: PropTypes.bool,
},
getDefaultProps: function() {
@ -75,6 +81,7 @@ const EntityTile = React.createClass({
presenceLastTs: 0,
showInviteButton: false,
suppressOnHover: false,
showPresence: true,
};
},
@ -99,7 +106,7 @@ const EntityTile = React.createClass({
render: function() {
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo,
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
);
let mainClassName = "mx_EntityTile ";
@ -114,15 +121,21 @@ const EntityTile = React.createClass({
mainClassName += " mx_EntityTile_hover";
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
let presenceLabel = null;
let nameClasses = 'mx_EntityTile_name';
if (this.props.showPresence) {
presenceLabel = <PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />;
nameClasses += ' mx_EntityTile_name_hover';
}
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12" />
<EmojiText element="div" className="mx_EntityTile_name mx_EntityTile_name_hover" dir="auto">
<EmojiText element="div" className={nameClasses} dir="auto">
{ name }
</EmojiText>
<PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
{presenceLabel}
</div>
);
} else {

View file

@ -153,7 +153,17 @@ module.exports = withMatrixClient(React.createClass({
},
getInitialState: function() {
return {menu: false, allReadAvatars: false, verified: null};
return {
// Whether the context menu is being displayed.
menu: false,
// Whether all read receipts are being displayed. If not, only display
// a truncation of them.
allReadAvatars: false,
// Whether the event's sender has been verified.
verified: null,
// Whether onRequestKeysClick has been called since mounting.
previouslyRequestedKeys: false,
};
},
componentWillMount: function() {
@ -394,6 +404,19 @@ module.exports = withMatrixClient(React.createClass({
});
},
onRequestKeysClick: function() {
this.setState({
// Indicate in the UI that the keys have been requested (this is expected to
// be reset if the component is mounted in the future).
previouslyRequestedKeys: true,
});
// Cancel any outgoing key request for this event and resend it. If a response
// is received for the request with the required keys, the event could be
// decrypted successfully.
this.props.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
},
onPermalinkClicked: function(e) {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
@ -460,6 +483,7 @@ module.exports = withMatrixClient(React.createClass({
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
const classes = classNames({
mx_EventTile: true,
@ -476,7 +500,7 @@ module.exports = withMatrixClient(React.createClass({
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
@ -536,6 +560,40 @@ module.exports = withMatrixClient(React.createClass({
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
const keyRequestHelpText =
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
<p>
{ this.state.previouslyRequestedKeys ?
_t( 'Your key share request has been sent - please check your other devices ' +
'for key share requests.') :
_t( 'Key share requests are sent to your other devices automatically. If you ' +
'rejected or dismissed the key share request on your other devices, click ' +
'here to request the keys for this session again.')
}
</p>
<p>
{ _t( 'If your other devices do not have the key for this message you will not ' +
'be able to decrypt them.')
}
</p>
</div>;
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
_t('Key request sent.') :
_t(
'<requestLink>Re-request encryption keys</requestLink> from your other devices.',
{},
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
);
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
const keyRequestInfo = isEncryptionFailure ?
<div className="mx_EventTile_keyRequestInfo">
<span className="mx_EventTile_keyRequestInfo_text">
{ keyRequestInfoContent }
</span>
<ToolTipButton helpText={keyRequestHelpText} />
</div> : null;
switch (this.props.tileShape) {
case 'notif': {
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
@ -629,6 +687,7 @@ module.exports = withMatrixClient(React.createClass({
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ keyRequestInfo }
{ editButton }
</div>
</div>

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -39,8 +39,8 @@ import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
import SdkConfig from '../../../SdkConfig';
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
@ -754,6 +754,7 @@ module.exports = withMatrixClient(React.createClass({
tiles.push(
<RoomTile key={room.roomId} room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
@ -860,10 +861,24 @@ module.exports = withMatrixClient(React.createClass({
const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = this.props.matrixClient.baseUrl;
let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[hsUrl];
}
let presenceLabel = null;
if (showPresence) {
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />;
}
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
roomMemberDetails = <div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b>
@ -876,18 +891,17 @@ module.exports = withMatrixClient(React.createClass({
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
{presenceLabel}
</div>
</div>;
}
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
@ -911,7 +925,7 @@ module.exports = withMatrixClient(React.createClass({
{ this._renderDevices() }
{ spinner }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,9 +18,9 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
const MatrixClientPeg = require("../../../MatrixClientPeg");
const sdk = require('../../../index');
const GeminiScrollbar = require('react-gemini-scrollbar');
const rate_limited_func = require('../../../ratelimitedfunc');
const CallHandler = require("../../../CallHandler");
@ -59,6 +59,14 @@ module.exports = React.createClass({
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl;
this._showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
},
componentWillUnmount: function() {
@ -345,7 +353,7 @@ module.exports = React.createClass({
const memberList = members.map((userId) => {
const m = this.memberDict[userId];
return (
<MemberTile key={userId} member={m} ref={userId} />
<MemberTile key={userId} member={m} ref={userId} showPresence={this._showPresence} />
);
});
@ -358,7 +366,10 @@ module.exports = React.createClass({
if (membership === "invite") {
const EntityTile = sdk.getComponent("rooms.EntityTile");
memberList.push(...this._getPending3PidInvites().map((e) => {
return <EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />;
return <EntityTile key={e.getStateKey()}
name={e.getContent().display_name}
suppressOnHover={true}
/>;
}));
}
@ -383,6 +394,7 @@ module.exports = React.createClass({
render: function() {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
let invitedSection = null;
if (this._getChildCountInvited() > 0) {
@ -411,14 +423,14 @@ module.exports = React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined}
/>
{ invitedSection }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>
);
},

View file

@ -30,6 +30,13 @@ module.exports = React.createClass({
propTypes: {
member: PropTypes.any.isRequired, // RoomMember
showPresence: PropTypes.bool,
},
getDefaultProps: function() {
return {
showPresence: true,
};
},
getInitialState: function() {
@ -99,7 +106,7 @@ module.exports = React.createClass({
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerStatus={powerStatus} />
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} />
);
},
});

View file

@ -185,10 +185,21 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId, dateTime: formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)},
);
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
if (this.props.member.userId === this.props.member.rawDisplayName) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId,
dateTime: dateString},
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{displayName: this.props.member.rawDisplayName,
userName: this.props.member.userId,
dateTime: dateString},
);
}
}
return (

View file

@ -20,7 +20,6 @@ const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar');
const MatrixClientPeg = require("../../../MatrixClientPeg");
const CallHandler = require('../../../CallHandler');
const dis = require("../../../dispatcher");
@ -77,9 +76,7 @@ module.exports = React.createClass({
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.name", this.onRoomName);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Event.decrypted", this.onEventDecrypted);
cli.on("accountData", this.onAccountData);
@ -161,12 +158,6 @@ module.exports = React.createClass({
});
}
break;
case 'on_room_read':
// Force an update because the notif count state is too deep to cause
// an update. This forces the local echo of reading notifs to be
// reflected by the RoomTiles.
this.forceUpdate();
break;
}
},
@ -177,9 +168,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
@ -243,14 +232,6 @@ module.exports = React.createClass({
}
},
onRoomName: function(room) {
this._delayedRefreshRoomList();
},
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
},
onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList();
},
@ -369,7 +350,7 @@ module.exports = React.createClass({
return Boolean(isRoomVisible[taggedRoom.roomId]);
});
if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) {
filteredLists[tagName] = filteredRooms;
}
@ -526,7 +507,8 @@ module.exports = React.createClass({
onShowMoreRooms: function() {
// kick gemini in the balls to get it to wake up
// XXX: uuuuuuugh.
this.refs.gemscroll.forceUpdate();
if (!this._gemScroll) return;
this._gemScroll.forceUpdate();
},
_getEmptyContent: function(section) {
@ -617,13 +599,18 @@ module.exports = React.createClass({
return ret;
},
_collectGemini(gemScroll) {
this._gemScroll = gemScroll;
},
render: function() {
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
<div className="mx_RoomList">
<RoomSubList list={[]}
extraTiles={this._makeGroupInviteTiles()}
@ -729,7 +716,7 @@ module.exports = React.createClass({
searchFilter={self.props.searchFilter}
onShowMoreRooms={self.onShowMoreRooms} />
</div>
</GeminiScrollbar>
</GeminiScrollbarWrapper>
);
},
});

View file

@ -704,8 +704,10 @@ module.exports = React.createClass({
{ Object.keys(userLevels).map(function(user, i) {
return (
<li className="mx_RoomSettings_userLevel" key={user}>
{ _t("%(user)s is a", {user: user}) }
<PowerSelector value={userLevels[user]} disabled={true} />
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</li>
);
}) }

View file

@ -21,6 +21,7 @@ const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const classNames = require('classnames');
import dis from '../../../dispatcher';
const MatrixClientPeg = require('../../../MatrixClientPeg');
import DMRoomMap from '../../../utils/DMRoomMap';
const sdk = require('../../../index');
@ -41,6 +42,8 @@ module.exports = React.createClass({
collapsed: PropTypes.bool.isRequired,
unread: PropTypes.bool.isRequired,
highlight: PropTypes.bool.isRequired,
// If true, apply mx_RoomTile_transparent class
transparent: PropTypes.bool,
isInvite: PropTypes.bool.isRequired,
incomingCall: PropTypes.object,
},
@ -56,7 +59,9 @@ module.exports = React.createClass({
hover: false,
badgeHover: false,
menuDisplayed: false,
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
@ -79,6 +84,20 @@ module.exports = React.createClass({
}
},
onRoomTimeline: function(ev, room) {
if (room !== this.props.room) return;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
onRoomName: function(room) {
if (room !== this.props.room) return;
this.setState({
roomName: this.props.room.name,
});
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
@ -87,6 +106,21 @@ module.exports = React.createClass({
}
},
onAction: function(payload) {
switch (payload.action) {
// XXX: slight hack in order to zero the notification count when a room
// is read. Ideally this state would be given to this via props (as we
// do with `unread`). This is still better than forceUpdating the entire
// RoomList when a room is read.
case 'on_room_read':
if (payload.roomId !== this.props.room.roomId) break;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
break;
}
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
@ -95,15 +129,46 @@ module.exports = React.createClass({
componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef);
},
componentWillReceiveProps: function(props) {
// XXX: This could be a lot better - this makes the assumption that
// the notification count may have changed when the properties of
// the room tile change.
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
// Do a simple shallow comparison of props and state to avoid unnecessary
// renders. The assumption made here is that only state and props are used
// in rendering this component and children.
//
// RoomList is frequently made to forceUpdate, so this decreases number of
// RoomTile renderings.
shouldComponentUpdate: function(newProps, newState) {
if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
return true;
}
if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
return true;
}
return false;
},
onClick: function(ev) {
@ -172,7 +237,7 @@ module.exports = React.createClass({
const myUserId = MatrixClientPeg.get().credentials.userId;
const me = this.props.room.currentState.members[myUserId];
const notificationCount = this.props.room.getUnreadNotificationCount();
const notificationCount = this.state.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
@ -188,6 +253,7 @@ module.exports = React.createClass({
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
});
const avatarClasses = classNames({
@ -199,9 +265,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
});
// XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k)
let name = this.props.room.name || this.props.room.roomId;
let name = this.state.roomName;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge;

View file

@ -19,7 +19,6 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal");
const sdk = require("../../../index");
import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar');
// A list capable of displaying entities which conform to the SearchableEntity
// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
@ -164,11 +163,12 @@ const SearchableEntityList = React.createClass({
</div>
);
}
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
list = (
<GeminiScrollbar autoshow={true}
<GeminiScrollbarWrapper autoshow={true}
className="mx_SearchableEntityList_listWrapper">
{ list }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
);
}