You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2025-07-30 02:21:17 +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:
@ -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: {
|
||||||
|
@ -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 : ""),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this._onScrollKeyPressed(ev);
|
this._onScrollKeyPressed(ev);
|
||||||
handled = true;
|
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}
|
||||||
/>;
|
/>;
|
||||||
|
@ -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."),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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}
|
||||||
|
@ -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:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(1);
|
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;
|
||||||
|
@ -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 }
|
||||||
|
@ -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() }><click to reveal></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>
|
||||||
|
@ -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 }
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
if (this.props.searchEnabled) {
|
||||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||||
onKeyUp={this._onInputKeyUp}
|
onKeyUp={this._onInputKeyUp}
|
||||||
onChange={this._onInputChange}
|
onChange={this._onInputChange}
|
||||||
value={this.state.searchQuery}
|
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
|
||||||
|
@ -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>
|
||||||
|
@ -25,23 +25,8 @@ 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)
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onPasswordChanged: function() {},
|
onPasswordChanged: function() {},
|
||||||
onPhoneCountryChanged: function() {},
|
onPhoneCountryChanged: function() {},
|
||||||
@ -51,29 +36,38 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
initialPhoneNumber: "",
|
initialPhoneNumber: "",
|
||||||
initialPassword: "",
|
initialPassword: "",
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
};
|
hsDomain: "",
|
||||||
},
|
}
|
||||||
|
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
this.state = {
|
||||||
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;
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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(()=>{
|
||||||
|
@ -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."),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
|
@ -265,9 +265,16 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onRoomStateMember: function(ev, state, member) {
|
onRoomStateMember: function(ev, state, member) {
|
||||||
|
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
|
||||||
|
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
|
||||||
|
{
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
constantTimeDispatcher.dispatch(
|
constantTimeDispatcher.dispatch(
|
||||||
"RoomTile.refresh", member.roomId, {}
|
"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"
|
||||||
|
@ -129,6 +129,8 @@ module.exports = React.createClass({
|
|||||||
console.error("Failed to get room visibility: " + err);
|
console.error("Failed to get room visibility: " + err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.scalarClient = null;
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
this.scalarClient = new ScalarAuthClient();
|
this.scalarClient = new ScalarAuthClient();
|
||||||
this.scalarClient.connect().done(() => {
|
this.scalarClient.connect().done(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
@ -137,6 +139,7 @@ module.exports = React.createClass({
|
|||||||
scalar_error: err
|
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,8 +768,10 @@ module.exports = React.createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var integrationsButton;
|
let integrationsButton;
|
||||||
var integrationsError;
|
let integrationsError;
|
||||||
|
|
||||||
|
if (this.scalarClient !== null) {
|
||||||
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
||||||
console.error(this.state.scalar_error);
|
console.error(this.state.scalar_error);
|
||||||
integrationsError = (
|
integrationsError = (
|
||||||
@ -791,11 +796,12 @@ module.exports = React.createClass({
|
|||||||
);
|
);
|
||||||
} 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 (
|
||||||
<div className="mx_RoomSettings">
|
<div className="mx_RoomSettings">
|
||||||
|
@ -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"
|
||||||
|
@ -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/#/((#|@|!).*)";
|
||||||
|
Reference in New Issue
Block a user