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:
commit
4285c395f5
29 changed files with 493 additions and 257 deletions
|
@ -135,17 +135,24 @@ module.exports = function (config) {
|
|||
},
|
||||
],
|
||||
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
|
||||
// cause stack overflows
|
||||
// (https://github.com/webpack/webpack/issues/1721), and
|
||||
// there is no need for webpack to parse them - they can
|
||||
// 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
|
||||
// tries to do voodoo with 'require' which upsets
|
||||
// webpack (https://github.com/webpack/webpack/issues/304)
|
||||
/sinon\/pkg\/sinon\.js$/,
|
||||
/sinon[\\\/]pkg[\\\/]sinon\.js$/,
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
|
|
@ -313,7 +313,7 @@ function _onAction(payload) {
|
|||
console.error("Conference call failed: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set up conference call",
|
||||
description: "Conference call failed.",
|
||||
description: "Conference call failed. " + ((err && err.message) ? err.message : ""),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ import emojione from 'emojione';
|
|||
import classNames from 'classnames';
|
||||
|
||||
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';
|
||||
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
|
@ -64,16 +67,23 @@ export function unicodeToImage(str) {
|
|||
* emoji.
|
||||
*
|
||||
* @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
|
||||
* @returns A img node with the corresponding emoji
|
||||
*/
|
||||
export function charactersToImageNode(alt, ...unicode) {
|
||||
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||
const fileName = unicode.map((u) => {
|
||||
return u.toString(16);
|
||||
}).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 {
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.innerHTML = html;
|
||||
|
|
|
@ -117,9 +117,10 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
break;
|
||||
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey) {
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
var action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
|
@ -129,13 +130,15 @@ export default React.createClass({
|
|||
|
||||
case KeyCode.PAGE_UP:
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -153,6 +156,9 @@ export default React.createClass({
|
|||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
}
|
||||
else if (this.refs.roomDirectory) {
|
||||
this.refs.roomDirectory.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -213,6 +219,7 @@ export default React.createClass({
|
|||
|
||||
case PageTypes.RoomDirectory:
|
||||
page_element = <RoomDirectory
|
||||
ref="roomDirectory"
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
config={this.props.config.roomDirectory}
|
||||
/>;
|
||||
|
|
|
@ -413,7 +413,7 @@ module.exports = React.createClass({
|
|||
console.error("Failed to leave room " + payload.room_id + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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."),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -388,6 +388,8 @@ module.exports = React.createClass({
|
|||
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 we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
|
|
@ -26,6 +26,7 @@ var q = require("q");
|
|||
var classNames = require("classnames");
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var ContentMessages = require("../../ContentMessages");
|
||||
var Modal = require("../../Modal");
|
||||
|
@ -953,7 +954,7 @@ module.exports = React.createClass({
|
|||
console.error("Failed to upload file " + file + " " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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() {
|
||||
self.setState({
|
||||
|
@ -1191,6 +1192,7 @@ module.exports = React.createClass({
|
|||
editingRoomSettings: false,
|
||||
forwardingMessage: null,
|
||||
});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -1736,7 +1738,7 @@ module.exports = React.createClass({
|
|||
var messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
manageReadReceipts={true}
|
||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
manageReadMarkers={true}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
|
|
|
@ -483,21 +483,25 @@ module.exports = React.createClass({
|
|||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.PAGE_UP:
|
||||
this.scrollRelative(-1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(-1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this.scrollRelative(1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -102,9 +102,6 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
statics: {
|
||||
// a map from room id to read marker event ID
|
||||
roomReadMarkerMap: {},
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
roomReadMarkerTsMap: {},
|
||||
},
|
||||
|
@ -121,10 +118,14 @@ var TimelinePanel = React.createClass({
|
|||
getInitialState: function() {
|
||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||
// but for now we just do it per room for simplicity.
|
||||
let initialReadMarker = null;
|
||||
if (this.props.manageReadMarkers) {
|
||||
var initialReadMarker =
|
||||
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|
||||
|| this._getCurrentReadReceipt();
|
||||
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||
if (readmarker){
|
||||
initialReadMarker = readmarker.getContent().event_id;
|
||||
} else {
|
||||
initialReadMarker = this._getCurrentReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -173,6 +174,7 @@ var TimelinePanel = React.createClass({
|
|||
debuglog("TimelinePanel: mounting");
|
||||
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.last_rm_sent_event_id = undefined;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
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.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
},
|
||||
|
@ -247,6 +250,7 @@ var TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
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) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||
updatedState.readMarkerVisible = false;
|
||||
updatedState.readMarkerEventId = lastEv.getId();
|
||||
|
@ -466,6 +471,21 @@ var TimelinePanel = React.createClass({
|
|||
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() {
|
||||
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
|
||||
// same one at the server repeatedly
|
||||
if (lastReadEventIndex > currentReadUpToEventIndex
|
||||
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
||||
if ((lastReadEventIndex > currentReadUpToEventIndex &&
|
||||
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();
|
||||
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
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.last_rm_sent_event_id = undefined;
|
||||
});
|
||||
|
||||
// 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.
|
||||
// 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[0].getTs()) {
|
||||
return -1;
|
||||
|
@ -729,7 +766,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END)
|
||||
{
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
|
@ -957,16 +996,12 @@ var TimelinePanel = React.createClass({
|
|||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
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
|
||||
// no change to the RM.
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
if (eventId === this.state.readMarkerEventId) {
|
||||
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
|
||||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||
|
@ -975,6 +1010,7 @@ var TimelinePanel = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// Do the local echo of the RM
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState({
|
||||
|
@ -1022,7 +1058,6 @@ var TimelinePanel = React.createClass({
|
|||
// events when viewing historical messages, we get stuck in a loop
|
||||
// of paginating our way through the entire history of the room.
|
||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
hidden={ this.props.hidden }
|
||||
|
|
|
@ -31,10 +31,14 @@ var SdkConfig = require('../../SdkConfig');
|
|||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
// if this looks like a release, use the 'version' from package.json; else use
|
||||
// the git sha.
|
||||
const REACT_SDK_VERSION =
|
||||
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
|
||||
// the git sha. Prepend version with v, to look like riot-web version
|
||||
const REACT_SDK_VERSION = 'dist' in package_json ? `v${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).
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
|
@ -44,6 +48,14 @@ const SETTINGS_LABELS = [
|
|||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: 'Hide read receipts'
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: "Don't send typing notifications",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
|
@ -211,7 +223,7 @@ module.exports = React.createClass({
|
|||
console.error("Failed to load user settings: " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar."
|
||||
title: "Failed to set avatar",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -271,7 +283,7 @@ module.exports = React.createClass({
|
|||
</div>,
|
||||
button: "Sign out",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
<button key="export" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
|
@ -354,8 +366,8 @@ module.exports = React.createClass({
|
|||
this.setState({email_add_pending: false});
|
||||
console.error("Unable to add email address " + email_address + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Unable to add email address"
|
||||
title: "Unable to add email address",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
});
|
||||
ReactDOM.findDOMNode(this.refs.add_email_input).blur();
|
||||
|
@ -379,8 +391,8 @@ module.exports = React.createClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Unable to remove contact information",
|
||||
title: "Unable to remove contact information",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
|
@ -420,8 +432,8 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify email address: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Unable to verify email address",
|
||||
title: "Unable to verify email address",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -763,6 +775,20 @@ module.exports = React.createClass({
|
|||
</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) {
|
||||
if (medium == 'msisdn') return 'Phone';
|
||||
return medium[0].toUpperCase() + medium.slice(1);
|
||||
|
@ -880,12 +906,12 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
var olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
const olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
// 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.
|
||||
var olmVersionString = "<not-enabled>";
|
||||
let olmVersionString = "<not-enabled>";
|
||||
if (olmVersion !== undefined) {
|
||||
olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2];
|
||||
olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -958,6 +984,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_advanced">
|
||||
Logged in as {this._me}
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
Access Token: <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }><click to reveal></span>
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
|
||||
</div>
|
||||
|
@ -965,8 +994,14 @@ module.exports = React.createClass({
|
|||
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
||||
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
|
||||
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
|
||||
? <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/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,13 +17,11 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../../index');
|
||||
var Login = require("../../../Login");
|
||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||
var CasLogin = require("../../views/login/CasLogin");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import url from 'url';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
|
@ -67,6 +65,7 @@ module.exports = React.createClass({
|
|||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
currentFlow: "m.login.password",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -129,23 +128,19 @@ module.exports = React.createClass({
|
|||
this.setState({ phoneNumber: phoneNumber });
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
onServerConfigChange: function(config) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl,
|
||||
let newState = {
|
||||
errorText: null, // reset err messages
|
||||
}, function() {
|
||||
self._initLoginLogic(newHsUrl);
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl,
|
||||
errorText: null, // reset err messages
|
||||
}, function() {
|
||||
self._initLoginLogic(null, newIsUrl);
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -161,25 +156,28 @@ module.exports = React.createClass({
|
|||
});
|
||||
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({
|
||||
enteredHomeserverUrl: hsUrl,
|
||||
enteredIdentityServerUrl: isUrl,
|
||||
busy: true,
|
||||
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() {
|
||||
|
@ -231,6 +229,13 @@ module.exports = React.createClass({
|
|||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
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 (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
|
@ -242,9 +247,11 @@ module.exports = React.createClass({
|
|||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsDomain={hsDomain}
|
||||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
|
@ -262,10 +269,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
|
@ -291,15 +299,14 @@ module.exports = React.createClass({
|
|||
<h2>Sign in
|
||||
{ loader }
|
||||
</h2>
|
||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
|
|
|
@ -47,6 +47,16 @@ export default React.createClass({
|
|||
children: React.PropTypes.node,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.priorActiveElement = document.activeElement;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this.priorActiveElement !== null) {
|
||||
this.priorActiveElement.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (e.keyCode === KeyCode.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
|
@ -67,7 +77,7 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
|
|
|
@ -308,8 +308,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
title: "Failed to invite",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -321,8 +321,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite user",
|
||||
title: "Failed to invite user",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -342,8 +342,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
title: "Failed to invite",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
|
|
@ -50,6 +50,12 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
|
@ -59,7 +65,7 @@ export default React.createClass({
|
|||
{this.props.description}
|
||||
</div>
|
||||
<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}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -248,13 +248,10 @@ export default class Dropdown extends React.Component {
|
|||
</MenuOption>
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.state.searchQuery) {
|
||||
options.push(
|
||||
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
|
||||
Type to search...
|
||||
</div>
|
||||
);
|
||||
if (options.length === 0) {
|
||||
return [<div className="mx_Dropdown_option">
|
||||
No results
|
||||
</div>];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
@ -267,16 +264,20 @@ export default class Dropdown extends React.Component {
|
|||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
if (this.props.searchEnabled) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
}
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{this._getMenuOptions()}
|
||||
</div>;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!currentValue) {
|
||||
const selectedChild = this.props.getShortOption ?
|
||||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
|
@ -313,6 +314,7 @@ Dropdown.propTypes = {
|
|||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
searchEnabled: React.PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
|
|
|
@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const MAX_DISPLAYED_ROWS = 2;
|
||||
|
||||
export default class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component {
|
|||
// Unicode Regional Indicator Symbol letter 'A'
|
||||
const RIS_A = 0x1F1E6;
|
||||
const ASCII_A = 65;
|
||||
return charactersToImageNode(iso2,
|
||||
return charactersToImageNode(iso2, true,
|
||||
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
||||
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
||||
);
|
||||
|
@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component {
|
|||
displayedCountries = COUNTRIES;
|
||||
}
|
||||
|
||||
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
|
||||
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
|
||||
}
|
||||
|
||||
const options = displayedCountries.map((country) => {
|
||||
return <div key={country.iso2}>
|
||||
{this._flagImgForIso2(country.iso2)}
|
||||
|
@ -111,7 +105,7 @@ export default class CountryDropdown extends React.Component {
|
|||
return <Dropdown className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._flagImgForIso2}
|
||||
value={value}
|
||||
value={value} searchEnabled={true}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
|
|
|
@ -25,55 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects';
|
|||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
module.exports = React.createClass({displayName: '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,
|
||||
},
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
hsDomain: "",
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: this.props.initialUsername,
|
||||
password: this.props.initialPassword,
|
||||
phoneCountry: this.props.initialPhoneCountry,
|
||||
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;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onSubmitForm: function(ev) {
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
|
@ -81,29 +75,99 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onUsernameChanged: function(ev) {
|
||||
onUsernameChanged(ev) {
|
||||
this.setState({username: 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.props.onPhoneCountryChanged(country);
|
||||
},
|
||||
}
|
||||
|
||||
onPhoneNumberChanged: function(ev) {
|
||||
onPhoneNumberChanged(ev) {
|
||||
this.setState({phoneNumber: ev.target.value});
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
onPasswordChanged: function(ev) {
|
||||
onPasswordChanged(ev) {
|
||||
this.setState({password: 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;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
|
@ -119,29 +183,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
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 (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field mx_Login_username" type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
value={this.state.username} onChange={this.onUsernameChanged}
|
||||
placeholder="Email or user name" autoFocus />
|
||||
or
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
|
||||
className="mx_Login_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
/>
|
||||
<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 className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">I want to sign in with my</label>
|
||||
<Dropdown
|
||||
className="mx_Login_type_dropdown"
|
||||
value={this.state.loginType}
|
||||
onOptionChange={this.onLoginTypeChange}>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>Matrix ID</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>Email Address</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>Phone</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<br />
|
||||
{loginField}
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
|
@ -153,4 +213,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
</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;
|
||||
|
|
|
@ -27,8 +27,7 @@ module.exports = React.createClass({
|
|||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
onHsUrlChanged: React.PropTypes.func,
|
||||
onIsUrlChanged: React.PropTypes.func,
|
||||
onServerConfigChange: React.PropTypes.func,
|
||||
|
||||
// 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.
|
||||
|
@ -50,8 +49,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHsUrlChanged: function() {},
|
||||
onIsUrlChanged: function() {},
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
withToggleButton: false,
|
||||
|
@ -75,7 +73,10 @@ module.exports = React.createClass({
|
|||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
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() {
|
||||
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
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
|
||||
});
|
||||
if (!visible) {
|
||||
this.props.onHsUrlChanged(this.props.defaultHsUrl);
|
||||
this.props.onIsUrlChanged(this.props.defaultIsUrl);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.props.defaultHsUrl,
|
||||
isUrl : this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.onHsUrlChanged(this.state.hs_url);
|
||||
this.props.onIsUrlChanged(this.state.is_url);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -346,7 +346,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<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 }
|
||||
</a>
|
||||
<div className="mx_MImageBody_size">
|
||||
|
@ -360,7 +360,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<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"/>
|
||||
Download {text}
|
||||
</a>
|
||||
|
|
|
@ -56,6 +56,7 @@ module.exports = React.createClass({
|
|||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: content.body && content.body.length > 0 ? content.body : 'Attachment',
|
||||
mxEvent: this.props.mxEvent,
|
||||
};
|
||||
|
||||
|
|
|
@ -284,6 +284,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
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 avatars = [];
|
||||
const receiptOffset = 15;
|
||||
|
|
|
@ -241,8 +241,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Kick error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to kick user",
|
||||
title: "Failed to kick",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
|
|
@ -355,6 +355,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
sendTyping(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
|
@ -509,7 +510,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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."),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
|
|||
var Modal = require("../../../Modal");
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
|
@ -311,7 +312,7 @@ export default React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
|
|
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
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";
|
||||
// if (this.props.currentlyActive) ago += " (now?)";
|
||||
return (
|
||||
|
|
|
@ -265,9 +265,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
constantTimeDispatcher.dispatch(
|
||||
"RoomTile.refresh", member.roomId, {}
|
||||
);
|
||||
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
|
||||
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
|
||||
{
|
||||
this._delayedRefreshRoomList();
|
||||
}
|
||||
else {
|
||||
constantTimeDispatcher.dispatch(
|
||||
"RoomTile.refresh", member.roomId, {}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
|
@ -449,11 +456,10 @@ module.exports = React.createClass({
|
|||
var panel = ReactDOM.findDOMNode(this);
|
||||
if (!panel) return null;
|
||||
|
||||
if (panel.classList.contains('gm-prevented')) {
|
||||
return panel;
|
||||
} else {
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
}
|
||||
// empirically, if we have gm-prevented for some reason, the scroll node
|
||||
// is still the 3rd child (i.e. the view child). This looks to be due
|
||||
// to vdh's improved resize updater logic...?
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
},
|
||||
|
||||
_whenScrolling: function(e) {
|
||||
|
@ -476,7 +482,7 @@ module.exports = React.createClass({
|
|||
// 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
|
||||
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
|
||||
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
|
||||
// as this is used to calculate the CSS fixed top position for the stickies
|
||||
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
|
||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||
|
||||
|
@ -599,7 +605,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<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 }>
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
label="Invites"
|
||||
|
|
|
@ -129,14 +129,17 @@ module.exports = React.createClass({
|
|||
console.error("Failed to get room visibility: " + err);
|
||||
});
|
||||
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
|
@ -490,7 +493,7 @@ module.exports = React.createClass({
|
|||
ev.preventDefault();
|
||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: this.scalarClient.hasCredentials() ?
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||
null,
|
||||
onFinished: ()=>{
|
||||
|
@ -765,36 +768,39 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
var integrationsButton;
|
||||
var 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>
|
||||
);
|
||||
}
|
||||
let integrationsButton;
|
||||
let integrationsError;
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
if (this.scalarClient !== null) {
|
||||
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 }>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -32,7 +33,10 @@ module.exports = React.createClass({
|
|||
<div className="mx_TopUnreadMessagesBar">
|
||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
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>
|
||||
<img className="mx_TopUnreadMessagesBar_close"
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
|
|
|
@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
|
|||
// anyone else really should be using matrix.to.
|
||||
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||
+ 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/#/((#|@|!).*)";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue