Merge branch 'develop' into bwindels/stylepreviewbar

This commit is contained in:
Bruno Windels 2019-04-17 11:06:21 +02:00
commit 22874f62ab
25 changed files with 280 additions and 139 deletions

View file

@ -0,0 +1,38 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../languageHandler";
export default class GenericErrorPage extends React.PureComponent {
static propTypes = {
message: PropTypes.string.isRequired,
};
render() {
return <div className='mx_GenericErrorPage'>
<div className='mx_GenericErrorPage_box'>
<h1>{_t("Error loading Riot")}</h1>
<p>{this.props.message}</p>
<p>{_t(
"If this is unexpected, please contact your system administrator " +
"or technical support representative.",
)}</p>
</div>
</div>;
}
}

View file

@ -29,13 +29,6 @@ export default class IndicatorScrollbar extends React.Component {
// scroll horizontally rather than vertically. This should only be used on components
// with no vertical scroll opportunity.
verticalScrollsHorizontally: PropTypes.bool,
// An object containing 2 numbers: xyThreshold and yReduction. xyThreshold is the amount
// of horizontal movement required in order to ignore any vertical changes in scroll, and
// only applies when verticalScrollsHorizontally is true. yReduction is the factor to
// multiply the vertical delta by when verticalScrollsHorizontally is true. The default
// behaviour is to have an xyThreshold of infinity and a yReduction of 0.8
scrollTolerances: PropTypes.object,
};
constructor(props) {
@ -127,20 +120,19 @@ export default class IndicatorScrollbar extends React.Component {
onMouseWheel = (e) => {
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
const xyThreshold = this.props.scrollTolerances
? this.props.scrollTolerances.xyThreshold
: Number.MAX_SAFE_INTEGER;
// xyThreshold is the amount of horizontal motion required for the component to
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
// strange ways. Should be positive.
const xyThreshold = 0;
const yReduction = this.props.scrollTolerances
? this.props.scrollTolerances.yReduction
: 0.8;
// yRetention is the factor multiplied by the vertical delta to try and reduce
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
const yRetention = 1.0;
// Don't apply vertical motion to horizontal scrolls. This is meant to eliminate
// trackpads causing excessive scroll motion.
if (e.deltaX >= xyThreshold) return;
// noinspection JSSuspiciousNameCombination
this._scrollElement.scrollLeft += e.deltaY * yReduction;
if (Math.abs(e.deltaX) < xyThreshold) {
// noinspection JSSuspiciousNameCombination
this._scrollElement.scrollLeft += e.deltaY * yRetention;
}
}
};

View file

@ -565,23 +565,6 @@ export default React.createClass({
},
});
break;
case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
if (this.state.collapsedRhs) {
setTimeout(()=>{
dis.dispatch({
action: 'show_right_panel',
});
dis.dispatch({
action: 'view_user',
member: payload.member,
});
}, 0);
}
break;
// different from view_user,
// this show the user panel outside of the context
// of a room, like a /user/<id> url
case 'view_user_info':
this._viewUser(payload.userId);
break;
@ -1820,7 +1803,7 @@ export default React.createClass({
},
_setPageSubtitle: function(subtitle='') {
document.title = `Riot ${subtitle}`;
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`;
},
updateStatusIndicator: function(state, prevState) {

View file

@ -272,6 +272,28 @@ module.exports = React.createClass({
return this.state.room ? this.state.room.roomId : this.state.roomId;
},
_getPermalinkCreatorForRoom: function(room) {
if (!this._permalinkCreators) this._permalinkCreators = {};
if (this._permalinkCreators[room.roomId]) return this._permalinkCreators[room.roomId];
this._permalinkCreators[room.roomId] = new RoomPermalinkCreator(room);
if (this.state.room && room.roomId === this.state.room.roomId) {
// We want to watch for changes in the creator for the primary room in the view, but
// don't need to do so for search results.
this._permalinkCreators[room.roomId].start();
} else {
this._permalinkCreators[room.roomId].load();
}
return this._permalinkCreators[room.roomId];
},
_stopAllPermalinkCreators: function() {
if (!this._permalinkCreators) return;
for (const roomId of Object.keys(this._permalinkCreators)) {
this._permalinkCreators[roomId].stop();
}
},
_onWidgetEchoStoreUpdate: function() {
this.setState({
showApps: this._shouldShowApps(this.state.room),
@ -436,9 +458,7 @@ module.exports = React.createClass({
}
// stop tracking room changes to format permalinks
if (this.state.permalinkCreator) {
this.state.permalinkCreator.stop();
}
this._stopAllPermalinkCreators();
if (this.refs.roomView) {
// disconnect the D&D event listeners from the room view. This
@ -651,11 +671,6 @@ module.exports = React.createClass({
this._loadMembersIfJoined(room);
this._calculateRecommendedVersion(room);
this._updateE2EStatus(room);
if (!this.state.permalinkCreator) {
const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.start();
this.setState({permalinkCreator});
}
},
_calculateRecommendedVersion: async function(room) {
@ -1169,6 +1184,7 @@ module.exports = React.createClass({
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = cli.getRoom(roomId);
if (!EventTile.haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
@ -1178,7 +1194,6 @@ module.exports = React.createClass({
if (this.state.searchScope === 'All') {
if (roomId != lastRoomId) {
const room = cli.getRoom(roomId);
// XXX: if we've left the room, we might not know about
// it. We should tell the js sdk to go and find out about
@ -1199,7 +1214,7 @@ module.exports = React.createClass({
searchResult={result}
searchHighlights={this.state.searchHighlights}
resultLink={resultLink}
permalinkCreator={this.state.permalinkCreator}
permalinkCreator={this._getPermalinkCreatorForRoom(room)}
onHeightChanged={onHeightChanged} />);
}
return ret;
@ -1715,7 +1730,7 @@ module.exports = React.createClass({
disabled={this.props.disabled}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
permalinkCreator={this.state.permalinkCreator}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
/>;
}
@ -1812,7 +1827,7 @@ module.exports = React.createClass({
showUrlPreview = {this.state.showUrlPreview}
className="mx_RoomView_messagePanel"
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.state.permalinkCreator}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier}
/>);

View file

@ -42,7 +42,12 @@ const PHASES_ENABLED = true;
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
_td("Invalid homeserver discovery response");
_td("Failed to get autodiscovery configuration from server");
_td("Invalid base_url for m.homeserver");
_td("Homeserver URL does not appear to be a valid Matrix homeserver");
_td("Invalid identity server discovery response");
_td("Invalid base_url for m.identity_server");
_td("Identity server URL does not appear to be a valid identity server");
_td("General failure");
/**

View file

@ -52,7 +52,7 @@ export default class RoomSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} />,
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
));
return tabs;

View file

@ -19,23 +19,30 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import HeaderButton from './HeaderButton';
import HeaderButtons from './HeaderButtons';
import RightPanel from '../../structures/RightPanel';
const GROUP_PHASES = [
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
];
const ROOM_PHASES = [
RightPanel.Phase.GroupRoomList,
RightPanel.Phase.GroupRoomInfo,
];
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, RightPanel.Phase.GroupMemberList);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onRoomsClicked = this._onRoomsClicked.bind(this);
}
onAction(payload) {
super.onAction(payload);
if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
if (payload.member) {
this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member});
} else {
@ -54,27 +61,26 @@ export default class GroupHeaderButtons extends HeaderButtons {
}
}
renderButtons() {
const groupPhases = [
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
];
const roomPhases = [
RightPanel.Phase.GroupRoomList,
RightPanel.Phase.GroupRoomInfo,
];
_onMembersClicked() {
this.togglePhase(RightPanel.Phase.GroupMemberList, GROUP_PHASES);
}
_onRoomsClicked() {
this.togglePhase(RightPanel.Phase.GroupRoomList, ROOM_PHASES);
}
renderButtons() {
return [
<HeaderButton key="groupMembersButton" name="groupMembersButton"
title={_t('Members')}
isHighlighted={this.isPhase(groupPhases)}
clickPhase={RightPanel.Phase.GroupMemberList}
isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this._onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="roomsButton" name="roomsButton"
title={_t('Rooms')}
isHighlighted={this.isPhase(roomPhases)}
clickPhase={RightPanel.Phase.GroupRoomList}
isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this._onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];

View file

@ -20,7 +20,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import Analytics from '../../../Analytics';
import AccessibleButton from '../elements/AccessibleButton';
@ -32,11 +31,7 @@ export default class HeaderButton extends React.Component {
onClick(ev) {
Analytics.trackEvent(...this.props.analytics);
dis.dispatch({
action: 'view_right_panel_phase',
phase: this.props.clickPhase,
fromHeader: true,
});
this.props.onClick();
}
render() {
@ -59,9 +54,8 @@ export default class HeaderButton extends React.Component {
HeaderButton.propTypes = {
// Whether this button is highlighted
isHighlighted: PropTypes.bool.isRequired,
// The phase to swap to when the button is clicked
clickPhase: PropTypes.string.isRequired,
// click handler
onClick: PropTypes.func.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event

View file

@ -40,14 +40,36 @@ export default class HeaderButtons extends React.Component {
dis.unregister(this.dispatcherRef);
}
componentDidUpdate(prevProps) {
if (!prevProps.collapsedRhs && this.props.collapsedRhs) {
this.setState({
phase: null,
});
}
}
setPhase(phase, extras) {
// TODO: delay?
if (this.props.collapsedRhs) {
dis.dispatch({
action: 'show_right_panel',
});
}
dis.dispatch(Object.assign({
action: 'view_right_panel_phase',
phase: phase,
}, extras));
}
togglePhase(phase, validPhases = [phase]) {
if (validPhases.includes(this.state.phase)) {
dis.dispatch({
action: 'hide_right_panel',
});
} else {
this.setPhase(phase);
}
}
isPhase(phases) {
if (this.props.collapsedRhs) {
return false;
@ -61,28 +83,9 @@ export default class HeaderButtons extends React.Component {
onAction(payload) {
if (payload.action === "view_right_panel_phase") {
// only actions coming from header buttons should collapse the right panel
if (this.state.phase === payload.phase && payload.fromHeader) {
dis.dispatch({
action: 'hide_right_panel',
});
this.setState({
phase: null,
});
} else {
if (this.props.collapsedRhs && payload.fromHeader) {
dis.dispatch({
action: 'show_right_panel',
});
// emit payload again as the RightPanel didn't exist up
// till show_right_panel, just without the fromHeader flag
// as that would hide the right panel again
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
}
this.setState({
phase: payload.phase,
});
}
this.setState({
phase: payload.phase,
});
}
}

View file

@ -19,28 +19,33 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import HeaderButton from './HeaderButton';
import HeaderButtons from './HeaderButtons';
import RightPanel from '../../structures/RightPanel';
const MEMBER_PHASES = [
RightPanel.Phase.RoomMemberList,
RightPanel.Phase.RoomMemberInfo,
RightPanel.Phase.Room3pidMemberInfo,
];
export default class RoomHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, RightPanel.Phase.RoomMemberList);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onFilesClicked = this._onFilesClicked.bind(this);
this._onNotificationsClicked = this._onNotificationsClicked.bind(this);
}
onAction(payload) {
super.onAction(payload);
if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
if (payload.member) {
this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RightPanel.Phase.RoomMemberList);
}
} else if (payload.action === "view_room") {
} else if (payload.action === "view_room" && !this.props.collapsedRhs) {
this.setPhase(RightPanel.Phase.RoomMemberList);
} else if (payload.action === "view_3pid_invite") {
if (payload.event) {
@ -51,30 +56,36 @@ export default class RoomHeaderButtons extends HeaderButtons {
}
}
renderButtons() {
const membersPhases = [
RightPanel.Phase.RoomMemberList,
RightPanel.Phase.RoomMemberInfo,
RightPanel.Phase.Room3pidMemberInfo,
];
_onMembersClicked() {
this.togglePhase(RightPanel.Phase.RoomMemberList, MEMBER_PHASES);
}
_onFilesClicked() {
this.togglePhase(RightPanel.Phase.FilePanel);
}
_onNotificationsClicked() {
this.togglePhase(RightPanel.Phase.NotificationPanel);
}
renderButtons() {
return [
<HeaderButton key="membersButton" name="membersButton"
title={_t('Members')}
isHighlighted={this.isPhase(membersPhases)}
clickPhase={RightPanel.Phase.RoomMemberList}
isHighlighted={this.isPhase(MEMBER_PHASES)}
onClick={this._onMembersClicked}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RightPanel.Phase.FilePanel)}
clickPhase={RightPanel.Phase.FilePanel}
onClick={this._onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanel.Phase.NotificationPanel)}
clickPhase={RightPanel.Phase.NotificationPanel}
onClick={this._onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];

View file

@ -33,12 +33,7 @@ const MAX_ROOMS = 20;
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
const tolerances = SettingsStore.getValue("breadcrumb_scroll_tolerances");
this.state = {rooms: [], scrollTolerances: tolerances};
// Record this for debugging purposes
console.log("Breadcrumbs scroll tolerances:", tolerances);
this.state = {rooms: []};
this.onAction = this.onAction.bind(this);
this._dispatcherRef = null;
@ -343,8 +338,7 @@ export default class RoomBreadcrumbs extends React.Component {
});
return (
<IndicatorScrollbar ref="scroller" className="mx_RoomBreadcrumbs"
trackHorizontalOverflow={true} verticalScrollsHorizontally={true}
scrollTolerances={this.state.scrollTolerances}>
trackHorizontalOverflow={true} verticalScrollsHorizontally={true}>
{ avatars }
</IndicatorScrollbar>
);

View file

@ -187,12 +187,17 @@ export default class KeyBackupPanel extends React.PureComponent {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t(
"This device is <b>not backing up your keys</b>.", {},
"This device is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
<p>{_t(
"Connect this device to key backup before signing out to avoid " +
"losing any keys that may only be on this device.",
)}</p>
</div>;
restoreButtonCaption = _t("Use key backup");
restoreButtonCaption = _t("Connect this device to Key Backup");
}
let uploadStatus;
@ -221,7 +226,10 @@ export default class KeyBackupPanel extends React.PureComponent {
{sub}
</span>;
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
const fromThisDevice = sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key();
const fromThisDevice = (
sig.device &&
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
);
let sigStatus;
if (!sig.device) {
sigStatus = _t(

View file

@ -21,10 +21,12 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher";
export default class AdvancedRoomSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
@ -41,9 +43,21 @@ export default class AdvancedRoomSettingsTab extends React.Component {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room.getRecommendedVersion().then((v) => {
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
const additionalStateChanges = {};
const createEvent = room.currentState.getStateEvents("m.room.create", "");
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
if (predecessor && predecessor.room_id) {
additionalStateChanges['oldRoomId'] = predecessor.room_id;
additionalStateChanges['oldEventId'] = predecessor.event_id;
additionalStateChanges['hasPreviousRoom'] = true;
}
this.setState({
upgraded: tombstone && tombstone.getContent().replacement_room,
upgradeRecommendation: v,
...additionalStateChanges,
});
});
}
@ -59,6 +73,18 @@ export default class AdvancedRoomSettingsTab extends React.Component {
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
};
_onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_room',
room_id: this.state.oldRoomId,
event_id: this.state.oldEventId,
});
this.props.closeSettingsFn();
};
render() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
@ -91,6 +117,18 @@ export default class AdvancedRoomSettingsTab extends React.Component {
);
}
let oldRoomLink;
if (this.state.hasPreviousRoom) {
let name = _t("this room");
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room && room.name) name = room.name;
oldRoomLink = (
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
{_t("View older messages in %(roomName)s.", {roomName: name})}
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
@ -108,6 +146,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
</div>
{oldRoomLink}
{roomUpgradeButton}
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>

View file

@ -136,7 +136,7 @@ export default class HelpUserSettingsTab extends React.Component {
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
default cover photo</a> is (C)&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
CC-BY-SA 4.0</a>. No warranties are given.