1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-07-28 15:22:05 +03:00

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

Conflicts:
	src/components/structures/RoomView.js
This commit is contained in:
Michael Telatynski
2017-04-24 18:36:33 +01:00
29 changed files with 493 additions and 257 deletions

View File

@ -135,17 +135,24 @@ module.exports = function (config) {
}, },
], ],
noParse: [ noParse: [
// for cross platform compatibility use [\\\/] as the path separator
// this ensures that the regex trips on both Windows and *nix
// don't parse the languages within highlight.js. They // don't parse the languages within highlight.js. They
// cause stack overflows // cause stack overflows
// (https://github.com/webpack/webpack/issues/1721), and // (https://github.com/webpack/webpack/issues/1721), and
// there is no need for webpack to parse them - they can // there is no need for webpack to parse them - they can
// just be included as-is. // just be included as-is.
/highlight\.js\/lib\/languages/, /highlight\.js[\\\/]lib[\\\/]languages/,
// olm takes ages for webpack to process, and it's already heavily
// optimised, so there is little to gain by us uglifying it.
/olm[\\\/](javascript[\\\/])?olm\.js$/,
// also disable parsing for sinon, because it // also disable parsing for sinon, because it
// tries to do voodoo with 'require' which upsets // tries to do voodoo with 'require' which upsets
// webpack (https://github.com/webpack/webpack/issues/304) // webpack (https://github.com/webpack/webpack/issues/304)
/sinon\/pkg\/sinon\.js$/, /sinon[\\\/]pkg[\\\/]sinon\.js$/,
], ],
}, },
resolve: { resolve: {

View File

@ -313,7 +313,7 @@ function _onAction(payload) {
console.error("Conference call failed: " + err); console.error("Conference call failed: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to set up conference call", title: "Failed to set up conference call",
description: "Conference call failed.", description: "Conference call failed. " + ((err && err.message) ? err.message : ""),
}); });
}); });
} }

View File

@ -25,6 +25,9 @@ import emojione from 'emojione';
import classNames from 'classnames'; import classNames from 'classnames';
emojione.imagePathSVG = 'emojione/svg/'; emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
emojione.imageType = 'svg'; emojione.imageType = 'svg';
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
@ -64,16 +67,23 @@ export function unicodeToImage(str) {
* emoji. * emoji.
* *
* @param alt {string} String to use for the image alt text * @param alt {string} String to use for the image alt text
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
* @param unicode {integer} One or more integers representing unicode characters * @param unicode {integer} One or more integers representing unicode characters
* @returns A img node with the corresponding emoji * @returns A img node with the corresponding emoji
*/ */
export function charactersToImageNode(alt, ...unicode) { export function charactersToImageNode(alt, useSvg, ...unicode) {
const fileName = unicode.map((u) => { const fileName = unicode.map((u) => {
return u.toString(16); return u.toString(16);
}).join('-'); }).join('-');
return <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>; const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
const fileType = useSvg ? 'svg' : 'png';
return <img
alt={alt}
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
/>;
} }
export function stripParagraphs(html: string): string { export function stripParagraphs(html: string): string {
const contentDiv = document.createElement('div'); const contentDiv = document.createElement('div');
contentDiv.innerHTML = html; contentDiv.innerHTML = html;

View File

@ -117,9 +117,10 @@ export default React.createClass({
} }
break; break;
case KeyCode.UP: case KeyCode.UP:
case KeyCode.DOWN: case KeyCode.DOWN:
if (ev.altKey) { if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
var action = ev.keyCode == KeyCode.UP ? var action = ev.keyCode == KeyCode.UP ?
'view_prev_room' : 'view_next_room'; 'view_prev_room' : 'view_next_room';
dis.dispatch({action: action}); dis.dispatch({action: action});
@ -129,13 +130,15 @@ export default React.createClass({
case KeyCode.PAGE_UP: case KeyCode.PAGE_UP:
case KeyCode.PAGE_DOWN: case KeyCode.PAGE_DOWN:
this._onScrollKeyPressed(ev); if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
handled = true; this._onScrollKeyPressed(ev);
handled = true;
}
break; break;
case KeyCode.HOME: case KeyCode.HOME:
case KeyCode.END: case KeyCode.END:
if (ev.ctrlKey) { if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this._onScrollKeyPressed(ev); this._onScrollKeyPressed(ev);
handled = true; handled = true;
} }
@ -153,6 +156,9 @@ export default React.createClass({
if (this.refs.roomView) { if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev); this.refs.roomView.handleScrollKey(ev);
} }
else if (this.refs.roomDirectory) {
this.refs.roomDirectory.handleScrollKey(ev);
}
}, },
render: function() { render: function() {
@ -213,6 +219,7 @@ export default React.createClass({
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
page_element = <RoomDirectory page_element = <RoomDirectory
ref="roomDirectory"
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory} config={this.props.config.roomDirectory}
/>; />;

View File

@ -413,7 +413,7 @@ module.exports = React.createClass({
console.error("Failed to leave room " + payload.room_id + " " + err); console.error("Failed to leave room " + payload.room_id + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to leave room", title: "Failed to leave room",
description: "Server may be unavailable, overloaded, or you hit a bug." description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."),
}); });
}); });
} }

View File

@ -388,6 +388,8 @@ module.exports = React.createClass({
isVisibleReadMarker = visible; isVisibleReadMarker = visible;
} }
// XXX: there should be no need for a ghost tile - we should just use a
// a dispatch (user_activity_end) to start the RM animation.
if (eventId == this.currentGhostEventId) { if (eventId == this.currentGhostEventId) {
// if we're showing an animation, continue to show it. // if we're showing an animation, continue to show it.
ret.push(this._getReadMarkerGhostTile()); ret.push(this._getReadMarkerGhostTile());

View File

@ -26,6 +26,7 @@ var q = require("q");
var classNames = require("classnames"); var classNames = require("classnames");
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var ContentMessages = require("../../ContentMessages"); var ContentMessages = require("../../ContentMessages");
var Modal = require("../../Modal"); var Modal = require("../../Modal");
@ -953,7 +954,7 @@ module.exports = React.createClass({
console.error("Failed to upload file " + file + " " + error); console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to upload file", title: "Failed to upload file",
description: "Server may be unavailable, overloaded, or the file too big", description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"),
}); });
}); });
}, },
@ -1040,7 +1041,7 @@ module.exports = React.createClass({
console.error("Search failed: " + error); console.error("Search failed: " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Search failed", title: "Search failed",
description: "Server may be unavailable, overloaded, or search timed out :(" description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("),
}); });
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
@ -1191,6 +1192,7 @@ module.exports = React.createClass({
editingRoomSettings: false, editingRoomSettings: false,
forwardingMessage: null, forwardingMessage: null,
}); });
dis.dispatch({action: 'focus_composer'});
}, },
onLeaveClick: function() { onLeaveClick: function() {
@ -1736,7 +1738,7 @@ module.exports = React.createClass({
var messagePanel = ( var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={true} manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadMarkers={true} manageReadMarkers={true}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId} highlightedEventId={this.props.highlightedEventId}

View File

@ -483,21 +483,25 @@ module.exports = React.createClass({
handleScrollKey: function(ev) { handleScrollKey: function(ev) {
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.PAGE_UP: case KeyCode.PAGE_UP:
this.scrollRelative(-1); if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(-1);
}
break; break;
case KeyCode.PAGE_DOWN: case KeyCode.PAGE_DOWN:
this.scrollRelative(1); if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(1);
}
break; break;
case KeyCode.HOME: case KeyCode.HOME:
if (ev.ctrlKey) { if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToTop(); this.scrollToTop();
} }
break; break;
case KeyCode.END: case KeyCode.END:
if (ev.ctrlKey) { if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToBottom(); this.scrollToBottom();
} }
break; break;

View File

@ -102,9 +102,6 @@ var TimelinePanel = React.createClass({
}, },
statics: { statics: {
// a map from room id to read marker event ID
roomReadMarkerMap: {},
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
roomReadMarkerTsMap: {}, roomReadMarkerTsMap: {},
}, },
@ -121,10 +118,14 @@ var TimelinePanel = React.createClass({
getInitialState: function() { getInitialState: function() {
// XXX: we could track RM per TimelineSet rather than per Room. // XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity. // but for now we just do it per room for simplicity.
let initialReadMarker = null;
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
var initialReadMarker = const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId] if (readmarker){
|| this._getCurrentReadReceipt(); initialReadMarker = readmarker.getContent().event_id;
} else {
initialReadMarker = this._getCurrentReadReceipt();
}
} }
return { return {
@ -173,6 +174,7 @@ var TimelinePanel = React.createClass({
debuglog("TimelinePanel: mounting"); debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined; this.last_rr_sent_event_id = undefined;
this.last_rm_sent_event_id = undefined;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -180,6 +182,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
this._initTimeline(this.props); this._initTimeline(this.props);
}, },
@ -247,6 +250,7 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.redaction", this.onRoomRedaction); client.removeListener("Room.redaction", this.onRoomRedaction);
client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
} }
}, },
@ -414,6 +418,7 @@ var TimelinePanel = React.createClass({
} else if(lastEv && this.getReadMarkerPosition() === 0) { } else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM // we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle // immediately, to save a later render cycle
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false; updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId(); updatedState.readMarkerEventId = lastEv.getId();
@ -466,6 +471,21 @@ var TimelinePanel = React.createClass({
this._reloadEvents(); this._reloadEvents();
}, },
onAccountData: function(ev, room) {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
if (ev.getType() !== "m.fully_read") return;
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
// this mechanism of determining where the RM is relative to the view-port with
// one supported by the server (the client needs more than an event ID).
this.setState({
readMarkerEventId: ev.getContent().event_id,
}, this.props.onReadMarkerUpdated);
},
sendReadReceipt: function() { sendReadReceipt: function() {
if (!this.refs.messagePanel) return; if (!this.refs.messagePanel) return;
@ -505,12 +525,29 @@ var TimelinePanel = React.createClass({
// we also remember the last read receipt we sent to avoid spamming the // we also remember the last read receipt we sent to avoid spamming the
// same one at the server repeatedly // same one at the server repeatedly
if (lastReadEventIndex > currentReadUpToEventIndex if ((lastReadEventIndex > currentReadUpToEventIndex &&
&& this.last_rr_sent_event_id != lastReadEvent.getId()) { this.last_rr_sent_event_id != lastReadEvent.getId()) ||
this.last_rm_sent_event_id != this.state.readMarkerEventId) {
this.last_rr_sent_event_id = lastReadEvent.getId(); this.last_rr_sent_event_id = lastReadEvent.getId();
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => { this.last_rm_sent_event_id = this.state.readMarkerEventId;
MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId,
this.state.readMarkerEventId,
lastReadEvent
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED') {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent
).catch(() => {
this.last_rr_sent_event_id = undefined;
});
}
// it failed, so allow retries next time the user is active // it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined; this.last_rr_sent_event_id = undefined;
this.last_rm_sent_event_id = undefined;
}); });
// do a quick-reset of our unreadNotificationCount to avoid having // do a quick-reset of our unreadNotificationCount to avoid having
@ -707,7 +744,7 @@ var TimelinePanel = React.createClass({
// the messagePanel doesn't know where the read marker is. // the messagePanel doesn't know where the read marker is.
// if we know the timestamp of the read marker, make a guess based on that. // if we know the timestamp of the read marker, make a guess based on that.
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId]; const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
if (rmTs && this.state.events.length > 0) { if (rmTs && this.state.events.length > 0) {
if (rmTs < this.state.events[0].getTs()) { if (rmTs < this.state.events[0].getTs()) {
return -1; return -1;
@ -729,7 +766,9 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the // jump to the live timeline on ctrl-end, rather than the end of the
// timeline window. // timeline window.
if (ev.ctrlKey && ev.keyCode == KeyCode.END) { if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
ev.keyCode == KeyCode.END)
{
this.jumpToLiveTimeline(); this.jumpToLiveTimeline();
} else { } else {
this.refs.messagePanel.handleScrollKey(ev); this.refs.messagePanel.handleScrollKey(ev);
@ -957,16 +996,12 @@ var TimelinePanel = React.createClass({
_setReadMarker: function(eventId, eventTs, inhibitSetState) { _setReadMarker: function(eventId, eventTs, inhibitSetState) {
var roomId = this.props.timelineSet.room.roomId; var roomId = this.props.timelineSet.room.roomId;
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) { // don't update the state (and cause a re-render) if there is
// don't update the state (and cause a re-render) if there is // no change to the RM.
// no change to the RM. if (eventId === this.state.readMarkerEventId) {
return; return;
} }
// ideally we'd sync these via the server, but for now just stash them
// in a map.
TimelinePanel.roomReadMarkerMap[roomId] = eventId;
// in order to later figure out if the read marker is // in order to later figure out if the read marker is
// above or below the visible timeline, we stash the timestamp. // above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs; TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
@ -975,6 +1010,7 @@ var TimelinePanel = React.createClass({
return; return;
} }
// Do the local echo of the RM
// run the render cycle before calling the callback, so that // run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing. // getReadMarkerPosition() returns the right thing.
this.setState({ this.setState({
@ -1022,7 +1058,6 @@ var TimelinePanel = React.createClass({
// events when viewing historical messages, we get stuck in a loop // events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room. // of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
return ( return (
<MessagePanel ref="messagePanel" <MessagePanel ref="messagePanel"
hidden={ this.props.hidden } hidden={ this.props.hidden }

View File

@ -31,10 +31,14 @@ var SdkConfig = require('../../SdkConfig');
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
// if this looks like a release, use the 'version' from package.json; else use // if this looks like a release, use the 'version' from package.json; else use
// the git sha. // the git sha. Prepend version with v, to look like riot-web version
const REACT_SDK_VERSION = const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || '<local>';
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
// Simple method to help prettify GH Release Tags and Commit Hashes.
const GHVersionUrl = function(repo, token) {
const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`;
return `https://github.com/${repo}/${uriTail}`;
}
// Enumerate some simple 'flip a bit' UI settings (if any). // Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event // 'id' gives the key name in the im.vector.web.settings account data event
@ -44,6 +48,14 @@ const SETTINGS_LABELS = [
id: 'autoplayGifsAndVideos', id: 'autoplayGifsAndVideos',
label: 'Autoplay GIFs and videos', label: 'Autoplay GIFs and videos',
}, },
{
id: 'hideReadReceipts',
label: 'Hide read receipts'
},
{
id: 'dontSendTypingNotifications',
label: "Don't send typing notifications",
},
/* /*
{ {
id: 'alwaysShowTimestamps', id: 'alwaysShowTimestamps',
@ -211,7 +223,7 @@ module.exports = React.createClass({
console.error("Failed to load user settings: " + error); console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Can't load user settings", title: "Can't load user settings",
description: "Server may be unavailable or overloaded", description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"),
}); });
}); });
}, },
@ -252,8 +264,8 @@ module.exports = React.createClass({
console.error("Failed to set avatar: " + err); console.error("Failed to set avatar: " + err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Failed to set avatar",
description: "Failed to set avatar." description: ((err && err.message) ? err.message : "Operation failed"),
}); });
}); });
}, },
@ -271,7 +283,7 @@ module.exports = React.createClass({
</div>, </div>,
button: "Sign out", button: "Sign out",
extraButtons: [ extraButtons: [
<button className="mx_Dialog_primary" <button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
Export E2E room keys Export E2E room keys
</button> </button>
@ -354,8 +366,8 @@ module.exports = React.createClass({
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
console.error("Unable to add email address " + email_address + " " + err); console.error("Unable to add email address " + email_address + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Unable to add email address",
description: "Unable to add email address" description: ((err && err.message) ? err.message : "Operation failed"),
}); });
}); });
ReactDOM.findDOMNode(this.refs.add_email_input).blur(); ReactDOM.findDOMNode(this.refs.add_email_input).blur();
@ -379,8 +391,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err); console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Unable to remove contact information",
description: "Unable to remove contact information", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
}).done(); }).done();
} }
@ -420,8 +432,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err); console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Unable to verify email address",
description: "Unable to verify email address", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
} }
}); });
@ -763,6 +775,20 @@ module.exports = React.createClass({
</div>; </div>;
}, },
_showSpoiler: function(event) {
const target = event.target;
const hidden = target.getAttribute('data-spoiler');
target.innerHTML = hidden;
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
nameForMedium: function(medium) { nameForMedium: function(medium) {
if (medium == 'msisdn') return 'Phone'; if (medium == 'msisdn') return 'Phone';
return medium[0].toUpperCase() + medium.slice(1); return medium[0].toUpperCase() + medium.slice(1);
@ -880,12 +906,12 @@ module.exports = React.createClass({
</div>); </div>);
} }
var olmVersion = MatrixClientPeg.get().olmVersion; const olmVersion = MatrixClientPeg.get().olmVersion;
// If the olmVersion is not defined then either crypto is disabled, or // If the olmVersion is not defined then either crypto is disabled, or
// we are using a version old version of olm. We assume the former. // we are using a version old version of olm. We assume the former.
var olmVersionString = "<not-enabled>"; let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) { if (olmVersion !== undefined) {
olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2]; olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
} }
return ( return (
@ -958,6 +984,9 @@ module.exports = React.createClass({
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Logged in as {this._me} Logged in as {this._me}
</div> </div>
<div className="mx_UserSettings_advanced">
Access Token: <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;click to reveal&gt;</span>
</div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() } Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
</div> </div>
@ -965,8 +994,14 @@ module.exports = React.createClass({
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() } Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
matrix-react-sdk version: {REACT_SDK_VERSION}<br/> matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/> ? <a href={ GHVersionUrl('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) }>{REACT_SDK_VERSION}</a>
: REACT_SDK_VERSION
}<br/>
riot-web version: {(this.state.vectorVersion !== null)
? <a href={ GHVersionUrl('vector-im/riot-web', this.state.vectorVersion.split('-')[0]) }>{this.state.vectorVersion}</a>
: 'unknown'
}<br/>
olm version: {olmVersionString}<br/> olm version: {olmVersionString}<br/>
</div> </div>
</div> </div>

View File

@ -17,13 +17,11 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
var ReactDOM = require('react-dom'); import ReactDOM from 'react-dom';
var sdk = require('../../../index'); import url from 'url';
var Login = require("../../../Login"); import sdk from '../../../index';
var PasswordLogin = require("../../views/login/PasswordLogin"); import Login from '../../../Login';
var CasLogin = require("../../views/login/CasLogin");
var ServerConfig = require("../../views/login/ServerConfig");
/** /**
* A wire component which glues together login UI components and Login logic * A wire component which glues together login UI components and Login logic
@ -67,6 +65,7 @@ module.exports = React.createClass({
username: "", username: "",
phoneCountry: null, phoneCountry: null,
phoneNumber: "", phoneNumber: "",
currentFlow: "m.login.password",
}; };
}, },
@ -129,23 +128,19 @@ module.exports = React.createClass({
this.setState({ phoneNumber: phoneNumber }); this.setState({ phoneNumber: phoneNumber });
}, },
onHsUrlChanged: function(newHsUrl) { onServerConfigChange: function(config) {
var self = this; var self = this;
this.setState({ let newState = {
enteredHomeserverUrl: newHsUrl,
errorText: null, // reset err messages errorText: null, // reset err messages
}, function() { };
self._initLoginLogic(newHsUrl); if (config.hsUrl !== undefined) {
}); newState.enteredHomeserverUrl = config.hsUrl;
}, }
if (config.isUrl !== undefined) {
onIsUrlChanged: function(newIsUrl) { newState.enteredIdentityServerUrl = config.isUrl;
var self = this; }
this.setState({ this.setState(newState, function() {
enteredIdentityServerUrl: newIsUrl, self._initLoginLogic(config.hsUrl || null, config.isUrl);
errorText: null, // reset err messages
}, function() {
self._initLoginLogic(null, newIsUrl);
}); });
}, },
@ -161,25 +156,28 @@ module.exports = React.createClass({
}); });
this._loginLogic = loginLogic; this._loginLogic = loginLogic;
loginLogic.getFlows().then(function(flows) {
// old behaviour was to always use the first flow without presenting
// options. This works in most cases (we don't have a UI for multiple
// logins so let's skip that for now).
loginLogic.chooseFlow(0);
}, function(err) {
self._setStateFromError(err, false);
}).finally(function() {
self.setState({
busy: false
});
});
this.setState({ this.setState({
enteredHomeserverUrl: hsUrl, enteredHomeserverUrl: hsUrl,
enteredIdentityServerUrl: isUrl, enteredIdentityServerUrl: isUrl,
busy: true, busy: true,
loginIncorrect: false, loginIncorrect: false,
}); });
loginLogic.getFlows().then(function(flows) {
// old behaviour was to always use the first flow without presenting
// options. This works in most cases (we don't have a UI for multiple
// logins so let's skip that for now).
loginLogic.chooseFlow(0);
self.setState({
currentFlow: self._getCurrentFlowStep(),
});
}, function(err) {
self._setStateFromError(err, false);
}).finally(function() {
self.setState({
busy: false,
});
});
}, },
_getCurrentFlowStep: function() { _getCurrentFlowStep: function() {
@ -231,6 +229,13 @@ module.exports = React.createClass({
componentForStep: function(step) { componentForStep: function(step) {
switch (step) { switch (step) {
case 'm.login.password': case 'm.login.password':
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
// HSs that are not matrix.org may not be configured to have their
// domain name === domain part.
let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname;
if (hsDomain !== 'matrix.org') {
hsDomain = null;
}
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -242,9 +247,11 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsDomain={hsDomain}
/> />
); );
case 'm.login.cas': case 'm.login.cas':
const CasLogin = sdk.getComponent('login.CasLogin');
return ( return (
<CasLogin onSubmit={this.onCasLogin} /> <CasLogin onSubmit={this.onCasLogin} />
); );
@ -262,10 +269,11 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
var LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
var LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
var loginAsGuestJsx; var loginAsGuestJsx;
if (this.props.enableGuest) { if (this.props.enableGuest) {
@ -291,15 +299,14 @@ module.exports = React.createClass({
<h2>Sign in <h2>Sign in
{ loader } { loader }
</h2> </h2>
{ this.componentForStep(this._getCurrentFlowStep()) } { this.componentForStep(this.state.currentFlow) }
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.props.customHsUrl} customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl} customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged} onServerConfigChange={this.onServerConfigChange}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000}/> delayTimeMs={1000}/>
<div className="mx_Login_error"> <div className="mx_Login_error">
{ this.state.errorText } { this.state.errorText }

View File

@ -47,6 +47,16 @@ export default React.createClass({
children: React.PropTypes.node, children: React.PropTypes.node,
}, },
componentWillMount: function() {
this.priorActiveElement = document.activeElement;
},
componentWillUnmount: function() {
if (this.priorActiveElement !== null) {
this.priorActiveElement.focus();
}
},
_onKeyDown: function(e) { _onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) { if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation(); e.stopPropagation();
@ -67,7 +77,7 @@ export default React.createClass({
render: function() { render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
return ( return (
<div onKeyDown={this._onKeyDown} className={this.props.className}> <div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick} <AccessibleButton onClick={this._onCancelClick}

View File

@ -308,8 +308,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Failed to invite",
description: "Failed to invite", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
return null; return null;
}) })
@ -321,8 +321,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Failed to invite user",
description: "Failed to invite user", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
return null; return null;
}) })
@ -342,8 +342,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Failed to invite",
description: "Failed to invite", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
return null; return null;
}) })

View File

@ -50,6 +50,12 @@ export default React.createClass({
}; };
}, },
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
@ -59,7 +65,7 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}> <button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button} {this.props.button}
</button> </button>
</div> </div>

View File

@ -248,13 +248,10 @@ export default class Dropdown extends React.Component {
</MenuOption> </MenuOption>
); );
}); });
if (options.length === 0) {
if (!this.state.searchQuery) { return [<div className="mx_Dropdown_option">
options.push( No results
<div key="_searchprompt" className="mx_Dropdown_searchPrompt"> </div>];
Type to search...
</div>
);
} }
return options; return options;
} }
@ -267,16 +264,20 @@ export default class Dropdown extends React.Component {
let menu; let menu;
if (this.state.expanded) { if (this.state.expanded) {
currentValue = <input type="text" className="mx_Dropdown_option" if (this.props.searchEnabled) {
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress} currentValue = <input type="text" className="mx_Dropdown_option"
onKeyUp={this._onInputKeyUp} ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
onChange={this._onInputChange} onKeyUp={this._onInputKeyUp}
value={this.state.searchQuery} onChange={this._onInputChange}
/>; value={this.state.searchQuery}
/>;
}
menu = <div className="mx_Dropdown_menu" style={menuStyle}> menu = <div className="mx_Dropdown_menu" style={menuStyle}>
{this._getMenuOptions()} {this._getMenuOptions()}
</div>; </div>;
} else { }
if (!currentValue) {
const selectedChild = this.props.getShortOption ? const selectedChild = this.props.getShortOption ?
this.props.getShortOption(this.props.value) : this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value]; this.childrenByKey[this.props.value];
@ -313,6 +314,7 @@ Dropdown.propTypes = {
onOptionChange: React.PropTypes.func.isRequired, onOptionChange: React.PropTypes.func.isRequired,
// Called when the value of the search field changes // Called when the value of the search field changes
onSearchChange: React.PropTypes.func, onSearchChange: React.PropTypes.func,
searchEnabled: React.PropTypes.bool,
// Function that, given the key of an option, returns // Function that, given the key of an option, returns
// a node representing that option to be displayed in the // a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as // box itself as the currently-selected option (ie. as

View File

@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) {
return false; return false;
} }
const MAX_DISPLAYED_ROWS = 2;
export default class CountryDropdown extends React.Component { export default class CountryDropdown extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component {
// Unicode Regional Indicator Symbol letter 'A' // Unicode Regional Indicator Symbol letter 'A'
const RIS_A = 0x1F1E6; const RIS_A = 0x1F1E6;
const ASCII_A = 65; const ASCII_A = 65;
return charactersToImageNode(iso2, return charactersToImageNode(iso2, true,
RIS_A + (iso2.charCodeAt(0) - ASCII_A), RIS_A + (iso2.charCodeAt(0) - ASCII_A),
RIS_A + (iso2.charCodeAt(1) - ASCII_A), RIS_A + (iso2.charCodeAt(1) - ASCII_A),
); );
@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component {
displayedCountries = COUNTRIES; displayedCountries = COUNTRIES;
} }
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
}
const options = displayedCountries.map((country) => { const options = displayedCountries.map((country) => {
return <div key={country.iso2}> return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)} {this._flagImgForIso2(country.iso2)}
@ -111,7 +105,7 @@ export default class CountryDropdown extends React.Component {
return <Dropdown className={this.props.className} return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange} onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._flagImgForIso2} menuWidth={298} getShortOption={this._flagImgForIso2}
value={value} value={value} searchEnabled={true}
> >
{options} {options}
</Dropdown> </Dropdown>

View File

@ -25,55 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects';
/** /**
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
*/ */
module.exports = React.createClass({displayName: 'PasswordLogin', class PasswordLogin extends React.Component {
propTypes: { static defaultProps = {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onUsernameChanged: function() {},
onForgotPasswordClick: React.PropTypes.func, // fn() onPasswordChanged: function() {},
initialUsername: React.PropTypes.string, onPhoneCountryChanged: function() {},
initialPhoneCountry: React.PropTypes.string, onPhoneNumberChanged: function() {},
initialPhoneNumber: React.PropTypes.string, initialUsername: "",
initialPassword: React.PropTypes.string, initialPhoneCountry: "",
onUsernameChanged: React.PropTypes.func, initialPhoneNumber: "",
onPhoneCountryChanged: React.PropTypes.func, initialPassword: "",
onPhoneNumberChanged: React.PropTypes.func, loginIncorrect: false,
onPasswordChanged: React.PropTypes.func, hsDomain: "",
loginIncorrect: React.PropTypes.bool, }
},
getDefaultProps: function() { constructor(props) {
return { super(props);
onUsernameChanged: function() {}, this.state = {
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
initialUsername: "",
initialPhoneCountry: "",
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
};
},
getInitialState: function() {
return {
username: this.props.initialUsername, username: this.props.initialUsername,
password: this.props.initialPassword, password: this.props.initialPassword,
phoneCountry: this.props.initialPhoneCountry, phoneCountry: this.props.initialPhoneCountry,
phoneNumber: this.props.initialPhoneNumber, phoneNumber: this.props.initialPhoneNumber,
loginType: PasswordLogin.LOGIN_FIELD_MXID,
}; };
},
componentWillMount: function() { this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
}
componentWillMount() {
this._passwordField = null; this._passwordField = null;
}, }
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps(nextProps) {
if (!this.props.loginIncorrect && nextProps.loginIncorrect) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
field_input_incorrect(this._passwordField); field_input_incorrect(this._passwordField);
} }
}, }
onSubmitForm: function(ev) { onSubmitForm(ev) {
ev.preventDefault(); ev.preventDefault();
this.props.onSubmit( this.props.onSubmit(
this.state.username, this.state.username,
@ -81,29 +75,99 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
this.state.phoneNumber, this.state.phoneNumber,
this.state.password, this.state.password,
); );
}, }
onUsernameChanged: function(ev) { onUsernameChanged(ev) {
this.setState({username: ev.target.value}); this.setState({username: ev.target.value});
this.props.onUsernameChanged(ev.target.value); this.props.onUsernameChanged(ev.target.value);
}, }
onPhoneCountryChanged: function(country) { onLoginTypeChange(loginType) {
this.setState({
loginType: loginType,
username: "" // Reset because email and username use the same state
});
}
onPhoneCountryChanged(country) {
this.setState({phoneCountry: country}); this.setState({phoneCountry: country});
this.props.onPhoneCountryChanged(country); this.props.onPhoneCountryChanged(country);
}, }
onPhoneNumberChanged: function(ev) { onPhoneNumberChanged(ev) {
this.setState({phoneNumber: ev.target.value}); this.setState({phoneNumber: ev.target.value});
this.props.onPhoneNumberChanged(ev.target.value); this.props.onPhoneNumberChanged(ev.target.value);
}, }
onPasswordChanged: function(ev) { onPasswordChanged(ev) {
this.setState({password: ev.target.value}); this.setState({password: ev.target.value});
this.props.onPasswordChanged(ev.target.value); this.props.onPasswordChanged(ev.target.value);
}, }
render: function() { renderLoginField(loginType) {
switch(loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
return <input
className="mx_Login_field mx_Login_email"
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder="joe@example.com"
value={this.state.username}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
const mxidInputClasses = classNames({
"mx_Login_field": true,
"mx_Login_username": true,
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain),
});
let suffix = null;
if (this.props.hsDomain) {
suffix = <div className="mx_Login_username_suffix">
:{this.props.hsDomain}
</div>;
}
return <div className="mx_Login_username_group">
<div className="mx_Login_username_prefix">@</div>
<input
className={mxidInputClasses}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder="username"
value={this.state.username}
autoFocus
/>
{suffix}
</div>;
case PasswordLogin.LOGIN_FIELD_PHONE:
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry"
ref="phone_country"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
/>
<input
className="mx_Login_phoneNumberField mx_Login_field"
ref="phoneNumber"
key="phone_input"
type="text"
name="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder="Mobile phone number"
value={this.state.phoneNumber}
autoFocus
/>
</div>;
}
}
render() {
var forgotPasswordJsx; var forgotPasswordJsx;
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
@ -119,29 +183,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
error: this.props.loginIncorrect, error: this.props.loginIncorrect,
}); });
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType);
return ( return (
<div> <div>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
<input className="mx_Login_field mx_Login_username" type="text" <div className="mx_Login_type_container">
name="username" // make it a little easier for browser's remember-password <label className="mx_Login_type_label">I want to sign in with my</label>
value={this.state.username} onChange={this.onUsernameChanged} <Dropdown
placeholder="Email or user name" autoFocus /> className="mx_Login_type_dropdown"
or value={this.state.loginType}
<div className="mx_Login_phoneSection"> onOptionChange={this.onLoginTypeChange}>
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged} <span key={PasswordLogin.LOGIN_FIELD_MXID}>Matrix ID</span>
className="mx_Login_phoneCountry" <span key={PasswordLogin.LOGIN_FIELD_EMAIL}>Email Address</span>
value={this.state.phoneCountry} <span key={PasswordLogin.LOGIN_FIELD_PHONE}>Phone</span>
/> </Dropdown>
<input type="text" ref="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder="Mobile phone number"
className="mx_Login_phoneNumberField mx_Login_field"
value={this.state.phoneNumber}
name="phoneNumber"
/>
</div> </div>
<br /> {loginField}
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"
value={this.state.password} onChange={this.onPasswordChanged} value={this.state.password} onChange={this.onPasswordChanged}
@ -153,4 +213,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
</div> </div>
); );
} }
}); }
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: React.PropTypes.func, // fn()
initialUsername: React.PropTypes.string,
initialPhoneCountry: React.PropTypes.string,
initialPhoneNumber: React.PropTypes.string,
initialPassword: React.PropTypes.string,
onUsernameChanged: React.PropTypes.func,
onPhoneCountryChanged: React.PropTypes.func,
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
hsDomain: React.PropTypes.string,
};
module.exports = PasswordLogin;

View File

@ -27,8 +27,7 @@ module.exports = React.createClass({
displayName: 'ServerConfig', displayName: 'ServerConfig',
propTypes: { propTypes: {
onHsUrlChanged: React.PropTypes.func, onServerConfigChange: React.PropTypes.func,
onIsUrlChanged: React.PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults) // default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL. // they are used if the user has not overridden them with a custom URL.
@ -50,8 +49,7 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onHsUrlChanged: function() {}, onServerConfigChange: function() {},
onIsUrlChanged: function() {},
customHsUrl: "", customHsUrl: "",
customIsUrl: "", customIsUrl: "",
withToggleButton: false, withToggleButton: false,
@ -75,7 +73,10 @@ module.exports = React.createClass({
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
if (hsUrl === "") hsUrl = this.props.defaultHsUrl; if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
this.props.onHsUrlChanged(hsUrl); this.props.onServerConfigChange({
hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
}); });
}); });
}, },
@ -85,7 +86,10 @@ module.exports = React.createClass({
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
var isUrl = this.state.is_url.trim().replace(/\/$/, ""); var isUrl = this.state.is_url.trim().replace(/\/$/, "");
if (isUrl === "") isUrl = this.props.defaultIsUrl; if (isUrl === "") isUrl = this.props.defaultIsUrl;
this.props.onIsUrlChanged(isUrl); this.props.onServerConfigChange({
hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
}); });
}); });
}, },
@ -102,12 +106,16 @@ module.exports = React.createClass({
configVisible: visible configVisible: visible
}); });
if (!visible) { if (!visible) {
this.props.onHsUrlChanged(this.props.defaultHsUrl); this.props.onServerConfigChange({
this.props.onIsUrlChanged(this.props.defaultIsUrl); hsUrl : this.props.defaultHsUrl,
isUrl : this.props.defaultIsUrl,
});
} }
else { else {
this.props.onHsUrlChanged(this.state.hs_url); this.props.onServerConfigChange({
this.props.onIsUrlChanged(this.state.is_url); hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
} }
}, },

View File

@ -346,7 +346,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank"> <a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName } { fileName }
</a> </a>
<div className="mx_MImageBody_size"> <div className="mx_MImageBody_size">
@ -360,7 +360,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={contentUrl} target="_blank" rel="noopener"> <a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/> <img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
Download {text} Download {text}
</a> </a>

View File

@ -56,6 +56,7 @@ module.exports = React.createClass({
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: content.body && content.body.length > 0 ? content.body : 'Attachment',
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}; };

View File

@ -284,6 +284,12 @@ module.exports = WithMatrixClient(React.createClass({
}, },
getReadAvatars: function() { getReadAvatars: function() {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars"></span>);
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
const avatars = []; const avatars = [];
const receiptOffset = 15; const receiptOffset = 15;

View File

@ -241,8 +241,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err); console.error("Kick error: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Failed to kick",
description: "Failed to kick user", description: ((err && err.message) ? err.message : "Operation failed"),
}); });
} }
).finally(()=>{ ).finally(()=>{

View File

@ -355,6 +355,7 @@ export default class MessageComposerInput extends React.Component {
} }
sendTyping(isTyping) { sendTyping(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping( MatrixClientPeg.get().sendTyping(
this.props.room.roomId, this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT this.isTyping, TYPING_SERVER_TIMEOUT
@ -509,7 +510,7 @@ export default class MessageComposerInput extends React.Component {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Server error", title: "Server error",
description: "Server unavailable, overloaded, or something else went wrong.", description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
}); });
}); });
} }

View File

@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index'); var sdk = require('../../../index');
import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode"); var KeyCode = require("../../../KeyCode");
@ -311,7 +312,7 @@ export default React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Server error", title: "Server error",
description: "Server unavailable, overloaded, or something else went wrong.", description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
}); });
}); });
} }
@ -420,6 +421,7 @@ export default React.createClass({
}, },
sendTyping: function(isTyping) { sendTyping: function(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping( MatrixClientPeg.get().sendTyping(
this.props.room.roomId, this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT this.isTyping, TYPING_SERVER_TIMEOUT

View File

@ -75,7 +75,7 @@ module.exports = React.createClass({
render: function() { render: function() {
if (this.props.activeAgo >= 0) { if (this.props.activeAgo >= 0) {
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago"); var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo));
// var ago = this.getDuration(this.props.activeAgo) + " ago"; // var ago = this.getDuration(this.props.activeAgo) + " ago";
// if (this.props.currentlyActive) ago += " (now?)"; // if (this.props.currentlyActive) ago += " (now?)";
return ( return (

View File

@ -265,9 +265,16 @@ module.exports = React.createClass({
}, },
onRoomStateMember: function(ev, state, member) { onRoomStateMember: function(ev, state, member) {
constantTimeDispatcher.dispatch( if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
"RoomTile.refresh", member.roomId, {} ev.getPrevContent() && ev.getPrevContent().membership === "invite")
); {
this._delayedRefreshRoomList();
}
else {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}
}, },
onRoomMemberName: function(ev, member) { onRoomMemberName: function(ev, member) {
@ -449,11 +456,10 @@ module.exports = React.createClass({
var panel = ReactDOM.findDOMNode(this); var panel = ReactDOM.findDOMNode(this);
if (!panel) return null; if (!panel) return null;
if (panel.classList.contains('gm-prevented')) { // empirically, if we have gm-prevented for some reason, the scroll node
return panel; // is still the 3rd child (i.e. the view child). This looks to be due
} else { // to vdh's improved resize updater logic...?
return panel.children[2]; // XXX: Fragile! return panel.children[2]; // XXX: Fragile!
}
}, },
_whenScrolling: function(e) { _whenScrolling: function(e) {
@ -476,7 +482,7 @@ module.exports = React.createClass({
// Use the offset of the top of the scroll area from the window // Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window // Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -499,7 +505,7 @@ module.exports = React.createClass({
// Use the offset of the top of the scroll area from the window // Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window // Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -599,7 +605,7 @@ module.exports = React.createClass({
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" <GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll"> autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }> <div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] } <RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites" label="Invites"

View File

@ -129,14 +129,17 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err); console.error("Failed to get room visibility: " + err);
}); });
this.scalarClient = new ScalarAuthClient(); this.scalarClient = null;
this.scalarClient.connect().done(() => { if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.forceUpdate(); this.scalarClient = new ScalarAuthClient();
}, (err) => { this.scalarClient.connect().done(() => {
this.setState({ this.forceUpdate();
scalar_error: err }, (err) => {
this.setState({
scalar_error: err
});
}); });
}); }
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'ui_opacity',
@ -490,7 +493,7 @@ module.exports = React.createClass({
ev.preventDefault(); ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createDialog(IntegrationsManager, { Modal.createDialog(IntegrationsManager, {
src: this.scalarClient.hasCredentials() ? src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
null, null,
onFinished: ()=>{ onFinished: ()=>{
@ -765,36 +768,39 @@ module.exports = React.createClass({
</div>; </div>;
} }
var integrationsButton; let integrationsButton;
var integrationsError; let integrationsError;
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
</span>
);
}
if (this.scalarClient.hasCredentials()) { if (this.scalarClient !== null) {
integrationsButton = ( if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
</span>
);
}
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }> <div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
Manage Integrations Manage Integrations
</div> </div>
); );
} else if (this.state.scalar_error) { } else if (this.state.scalar_error) {
integrationsButton = ( integrationsButton = (
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }> <div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
Integrations Error <img src="img/warning.svg" width="17"/> Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError } { integrationsError }
</div> </div>
); );
} else { } else {
integrationsButton = ( integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}> <div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
Manage Integrations Manage Integrations
</div> </div>
); );
}
} }
return ( return (

View File

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -32,7 +33,10 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar"> <div className="mx_TopUnreadMessagesBar">
<div className="mx_TopUnreadMessagesBar_scrollUp" <div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}> onClick={this.props.onScrollUpClick}>
Jump to first unread message. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span> <img src="img/scrollto.svg" width="24" height="24"
alt="Scroll to unread messages"
title="Scroll to unread messages"/>
Jump to first unread message.
</div> </div>
<img className="mx_TopUnreadMessagesBar_close" <img className="mx_TopUnreadMessagesBar_close"
src="img/cancel.svg" width="18" height="18" src="img/cancel.svg" width="18" height="18"

View File

@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
// anyone else really should be using matrix.to. // anyone else really should be using matrix.to.
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
+ escapeRegExp(window.location.host + window.location.pathname) + "|" + escapeRegExp(window.location.host + window.location.pathname) + "|"
+ "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/"
+ ")(#.*)"; + ")(#.*)";
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";