Merge branch 'develop' into webrtc_settings

pull/21833/head
Michael Telatynski 2017-05-05 20:56:20 +01:00 committed by GitHub
commit 110ca22c4c
21 changed files with 283 additions and 268 deletions

View File

@ -64,7 +64,7 @@ module.exports = {
// to JSX. // to JSX.
ignorePattern: '^\\s*<', ignorePattern: '^\\s*<',
ignoreComments: true, ignoreComments: true,
code: 90, code: 120,
}], }],
"valid-jsdoc": ["warn"], "valid-jsdoc": ["warn"],
"new-cap": ["warn"], "new-cap": ["warn"],

View File

@ -32,8 +32,8 @@
}, },
"scripts": { "scripts": {
"reskindex": "scripts/reskindex.js -h header", "reskindex": "scripts/reskindex.js -h header",
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps", "build": "babel src -d lib --source-maps",
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps", "start": "babel src -w -d lib --source-maps",
"lint": "eslint src/", "lint": "eslint src/",
"lintall": "eslint src/ test/", "lintall": "eslint src/ test/",
"clean": "rimraf lib", "clean": "rimraf lib",
@ -53,7 +53,7 @@
"draft-js-export-markdown": "^0.2.0", "draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3", "emojione": "2.2.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "^3.1.2", "filesize": "3.5.6",
"flux": "^2.0.3", "flux": "^2.0.3",
"fuse.js": "^2.2.0", "fuse.js": "^2.2.0",
"glob": "^5.0.14", "glob": "^5.0.14",

View File

@ -1,22 +0,0 @@
#!/usr/bin/env node
var exec = require('child_process').exec;
// Makes sure the babel executable in the path is babel 6 (or greater), not
// babel 5, which it is if you upgrade from an older version of react-sdk and
// run 'npm install' since the package has changed to babel-cli, so 'babel'
// remains installed and the executable in node_modules/.bin remains as babel
// 5.
exec("babel -V", function (error, stdout, stderr) {
if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
console.log("\033[31m\033[1m"+
'*****************************************\n'+
'* matrix-react-sdk has moved to babel 6 *\n'+
'* Please "rm -rf node_modules && npm i" *\n'+
'* then restore links as appropriate *\n'+
'*****************************************\n'+
"\033[91m");
process.exit(1);
}
});

View File

@ -22,8 +22,8 @@ module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) { avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl( var url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
width, Math.floor(width * window.devicePixelRatio),
height, Math.floor(height * window.devicePixelRatio),
resizeMethod, resizeMethod,
false, false,
false false
@ -40,7 +40,9 @@ module.exports = {
avatarUrlForUser: function(user, width, height, resizeMethod) { avatarUrlForUser: function(user, width, height, resizeMethod) {
var url = ContentRepo.getHttpUriForMxc( var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
width, height, resizeMethod Math.floor(width * window.devicePixelRatio),
Math.floor(height * window.devicePixelRatio),
resizeMethod
); );
if (!url || url.length === 0) { if (!url || url.length === 0) {
return null; return null;
@ -57,4 +59,3 @@ module.exports = {
return 'img/' + images[total % images.length] + '.png'; return 'img/' + images[total % images.length] + '.png';
} }
}; };

View File

@ -47,7 +47,7 @@ class ConstantTimeDispatcher {
dispatch(type, arg, params) { dispatch(type, arg, params) {
if (!this.listeners[type] || !this.listeners[type][arg]) { if (!this.listeners[type] || !this.listeners[type][arg]) {
console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); //console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
return; return;
} }
this.listeners[type][arg].forEach(listener=>{ this.listeners[type][arg].forEach(listener=>{

View File

@ -49,7 +49,7 @@ import sdk from './index';
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
* turn will raise on_logged_in and will_start_client events. * turn will raise on_logged_in and will_start_client events.
* *
* It returns a promise which resolves when the above process completes. * @param {object} opts
* *
* @param {object} opts.realQueryParams: string->string map of the * @param {object} opts.realQueryParams: string->string map of the
* query-parameters extracted from the real query-string of the starting * query-parameters extracted from the real query-string of the starting
@ -67,6 +67,7 @@ import sdk from './index';
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
* true; defines the IS to use. * true; defines the IS to use.
* *
* @returns {Promise} a promise which resolves when the above process completes.
*/ */
export function loadSession(opts) { export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {}; const realQueryParams = opts.realQueryParams || {};
@ -127,7 +128,7 @@ export function loadSession(opts) {
function _loginWithToken(queryParams, defaultDeviceDisplayName) { function _loginWithToken(queryParams, defaultDeviceDisplayName) {
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
var client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: queryParams.homeserver, baseUrl: queryParams.homeserver,
}); });
@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// Not really sure where the right home for it is. // Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
var client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
}); });
@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
if (!localStorage) { if (!localStorage) {
return q(false); return q(false);
} }
const hs_url = localStorage.getItem("mx_hs_url"); const hsUrl = localStorage.getItem("mx_hs_url");
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
const access_token = localStorage.getItem("mx_access_token"); const accessToken = localStorage.getItem("mx_access_token");
const user_id = localStorage.getItem("mx_user_id"); const userId = localStorage.getItem("mx_user_id");
const device_id = localStorage.getItem("mx_device_id"); const deviceId = localStorage.getItem("mx_device_id");
let is_guest; let isGuest;
if (localStorage.getItem("mx_is_guest") !== null) { if (localStorage.getItem("mx_is_guest") !== null) {
is_guest = localStorage.getItem("mx_is_guest") === "true"; isGuest = localStorage.getItem("mx_is_guest") === "true";
} else { } else {
// legacy key name // legacy key name
is_guest = localStorage.getItem("matrix-is-guest") === "true"; isGuest = localStorage.getItem("matrix-is-guest") === "true";
} }
if (access_token && user_id && hs_url) { if (accessToken && userId && hsUrl) {
console.log("Restoring session for %s", user_id); console.log("Restoring session for %s", userId);
try { try {
setLoggedIn({ setLoggedIn({
userId: user_id, userId: userId,
deviceId: device_id, deviceId: deviceId,
accessToken: access_token, accessToken: accessToken,
homeserverUrl: hs_url, homeserverUrl: hsUrl,
identityServerUrl: is_url, identityServerUrl: isUrl,
guest: is_guest, guest: isGuest,
}); });
return q(true); return q(true);
} catch (e) { } catch (e) {
@ -273,9 +274,13 @@ export function initRtsClient(url) {
*/ */
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
console.log("setLoggedIn => %s (guest=%s) hs=%s",
credentials.userId, credentials.guest, console.log(
credentials.homeserverUrl); "setLoggedIn: mxid:", credentials.userId,
"deviceId:", credentials.deviceId,
"guest:", credentials.guest,
"hs:", credentials.homeserverUrl,
);
// This is dispatched to indicate that the user is still in the process of logging in // This is dispatched to indicate that the user is still in the process of logging in
// because `teamPromise` may take some time to resolve, breaking the assumption that // because `teamPromise` may take some time to resolve, breaking the assumption that
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
@ -352,7 +357,7 @@ export function logout() {
return; return;
} }
return MatrixClientPeg.get().logout().then(onLoggedOut, MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => { (err) => {
// Just throwing an error here is going to be very unhelpful // Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and // if you're trying to log out because your server's down and
@ -363,8 +368,8 @@ export function logout() {
// change your password). // change your password).
console.log("Failed to call logout API: token will not be invalidated"); console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut(); onLoggedOut();
} },
); ).done();
} }
/** /**
@ -420,7 +425,7 @@ export function stopMatrixClient() {
UserActivity.stop(); UserActivity.stop();
Presence.stop(); Presence.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop();
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.stopClient(); cli.stopClient();
cli.removeAllListeners(); cli.removeAllListeners();

View File

@ -15,11 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); import MatrixClientPeg from './MatrixClientPeg';
var PlatformPeg = require("./PlatformPeg"); import PlatformPeg from './PlatformPeg';
var TextForEvent = require('./TextForEvent'); import TextForEvent from './TextForEvent';
var Avatar = require('./Avatar'); import Avatar from './Avatar';
var dis = require("./dispatcher"); import dis from './dispatcher';
import sdk from './index';
import Modal from './Modal';
/* /*
* Dispatches: * Dispatches:
@ -29,7 +31,7 @@ var dis = require("./dispatcher");
* } * }
*/ */
var Notifier = { const Notifier = {
notifsByRoom: {}, notifsByRoom: {},
notificationMessageForEvent: function(ev) { notificationMessageForEvent: function(ev) {
@ -48,16 +50,16 @@ var Notifier = {
return; return;
} }
var msg = this.notificationMessageForEvent(ev); let msg = this.notificationMessageForEvent(ev);
if (!msg) return; if (!msg) return;
var title; let title;
if (!ev.sender || room.name == ev.sender.name) { if (!ev.sender || room.name === ev.sender.name) {
title = room.name; title = room.name;
// notificationMessageForEvent includes sender, // notificationMessageForEvent includes sender,
// but we already have the sender here // but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body; if (ev.getContent().body) msg = ev.getContent().body;
} else if (ev.getType() == 'm.room.member') { } else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need // context is all in the message here, we don't need
// to display sender info // to display sender info
title = room.name; title = room.name;
@ -68,7 +70,7 @@ var Notifier = {
if (ev.getContent().body) msg = ev.getContent().body; if (ev.getContent().body) msg = ev.getContent().body;
} }
var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop' ev.sender, 40, 40, 'crop'
) : null; ) : null;
@ -83,7 +85,7 @@ var Notifier = {
}, },
_playAudioNotification: function(ev, room) { _playAudioNotification: function(ev, room) {
var e = document.getElementById("messageAudio"); const e = document.getElementById("messageAudio");
if (e) { if (e) {
e.load(); e.load();
e.play(); e.play();
@ -95,7 +97,7 @@ var Notifier = {
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false; this.toolbarHidden = false;
this.isSyncing = false; this.isSyncing = false;
@ -104,7 +106,7 @@ var Notifier = {
stop: function() { stop: function() {
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
} }
this.isSyncing = false; this.isSyncing = false;
@ -121,7 +123,7 @@ var Notifier = {
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (global.localStorage) {
if(global.localStorage.getItem('audio_notifications_enabled') == null) { if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled()); this.setAudioEnabled(this.isEnabled());
} }
} }
@ -131,6 +133,16 @@ var Notifier = {
plaf.requestNotificationPermission().done((result) => { plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
const description = result === 'denied'
? 'Riot does not have permission to send you notifications'
+ ' - please check your browser settings'
: 'Riot was not given permission to send notifications'
+ ' - please try again';
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: 'Unable to enable Notifications',
description,
});
return; return;
} }
@ -141,7 +153,7 @@ var Notifier = {
if (callback) callback(); if (callback) callback();
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: true value: true,
}); });
}); });
// clear the notifications_hidden flag, so that if notifications are // clear the notifications_hidden flag, so that if notifications are
@ -152,7 +164,7 @@ var Notifier = {
global.localStorage.setItem('notifications_enabled', 'false'); global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: false value: false,
}); });
} }
}, },
@ -165,7 +177,7 @@ var Notifier = {
if (!global.localStorage) return true; if (!global.localStorage) return true;
var enabled = global.localStorage.getItem('notifications_enabled'); const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true; if (enabled === null) return true;
return enabled === 'true'; return enabled === 'true';
}, },
@ -173,12 +185,12 @@ var Notifier = {
setAudioEnabled: function(enable) { setAudioEnabled: function(enable) {
if (!global.localStorage) return; if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled', global.localStorage.setItem('audio_notifications_enabled',
enable ? 'true' : 'false'); enable ? 'true' : 'false');
}, },
isAudioEnabled: function(enable) { isAudioEnabled: function(enable) {
if (!global.localStorage) return true; if (!global.localStorage) return true;
var enabled = global.localStorage.getItem( const enabled = global.localStorage.getItem(
'audio_notifications_enabled'); 'audio_notifications_enabled');
// default to true if the popups are enabled // default to true if the popups are enabled
if (enabled === null) return this.isEnabled(); if (enabled === null) return this.isEnabled();
@ -192,7 +204,7 @@ var Notifier = {
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: this.isEnabled() value: this.isEnabled(),
}); });
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
@ -215,8 +227,7 @@ var Notifier = {
onSyncStateChange: function(state) { onSyncStateChange: function(state) {
if (state === "SYNCING") { if (state === "SYNCING") {
this.isSyncing = true; this.isSyncing = true;
} } else if (state === "STOPPED" || state === "ERROR") {
else if (state === "STOPPED" || state === "ERROR") {
this.isSyncing = false; this.isSyncing = false;
} }
}, },
@ -225,10 +236,10 @@ var Notifier = {
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
if (!room) return; if (!room) return;
if (!this.isSyncing) return; // don't alert for any messages initially if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) { if (actions && actions.notify) {
if (this.isEnabled()) { if (this.isEnabled()) {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);
@ -240,7 +251,7 @@ var Notifier = {
}, },
onRoomReceipt: function(ev, room) { onRoomReceipt: function(ev, room) {
if (room.getUnreadNotificationCount() == 0) { if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read, // ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether // but we have no way, given a read receipt, to know whether
// the receipt comes before or after an event, so we can't // the receipt comes before or after an event, so we can't
@ -255,7 +266,7 @@ var Notifier = {
} }
delete this.notifsByRoom[room.roomId]; delete this.notifsByRoom[room.roomId];
} }
} },
}; };
if (!global.mxNotifier) { if (!global.mxNotifier) {

View File

@ -32,7 +32,7 @@ class UserActivity {
start() { start() {
document.onmousedown = this._onUserActivity.bind(this); document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this); document.onmousemove = this._onUserActivity.bind(this);
document.onkeypress = this._onUserActivity.bind(this); document.onkeydown = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document // can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture. // itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is // also this needs to be the wheel event, not scroll, as scroll is
@ -50,7 +50,7 @@ class UserActivity {
stop() { stop() {
document.onmousedown = undefined; document.onmousedown = undefined;
document.onmousemove = undefined; document.onmousemove = undefined;
document.onkeypress = undefined; document.onkeydown = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this), window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true }); { passive: true, capture: true });
} }

View File

@ -226,10 +226,8 @@ export default React.createClass({
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
page_element = <RoomDirectory page_element = <RoomDirectory
ref="roomDirectory" ref="roomDirectory"
collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory} config={this.props.config.roomDirectory}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break; break;
case PageTypes.HomePage: case PageTypes.HomePage:

View File

@ -282,15 +282,16 @@ module.exports = React.createClass({
var isMembershipChange = (e) => e.getType() === 'm.room.member'; var isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) { for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i]; let mxEv = this.props.events[i];
var wantTile = true; let wantTile = true;
var eventId = mxEv.getId(); let eventId = mxEv.getId();
let readMarkerInMels = false;
if (!EventTile.haveTileForEvent(mxEv)) { if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false; wantTile = false;
} }
var last = (i == lastShownEventIndex); let last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary, ignore if redacted // Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && if (isMembershipChange(mxEv) &&
@ -332,6 +333,9 @@ module.exports = React.createClass({
let eventTiles = summarisedEvents.map( let eventTiles = summarisedEvents.map(
(e) => { (e) => {
if (e.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form // In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous // of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the // one was itself. This way, the timestamp of the previous event === the
@ -350,12 +354,16 @@ module.exports = React.createClass({
<MemberEventListSummary <MemberEventListSummary
key={key} key={key}
events={summarisedEvents} events={summarisedEvents}
data-scroll-token={eventId}
onToggle={this._onWidgetLoad} // Update scroll state onToggle={this._onWidgetLoad} // Update scroll state
> >
{eventTiles} {eventTiles}
</MemberEventListSummary> </MemberEventListSummary>
); );
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
}
continue; continue;
} }
@ -464,7 +472,7 @@ module.exports = React.createClass({
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}> data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
onWidgetLoad={this._onWidgetLoad} onWidgetLoad={this._onWidgetLoad}

View File

@ -1276,13 +1276,7 @@ module.exports = React.createClass({
return; return;
} }
var pos = this.refs.messagePanel.getReadMarkerPosition(); const showBar = this.refs.messagePanel.canJumpToReadMarker();
// we want to show the bar if the read-marker is off the top of the
// screen.
// If pos is null, the event might not be paginated, so show the unread bar!
var showBar = pos < 0 || pos === null;
if (this.state.showTopUnreadMessagesBar != showBar) { if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar}, this.setState({showTopUnreadMessagesBar: showBar},
this.onChildResize); this.onChildResize);

View File

@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
* It also provides a hook which allows parents to provide more list elements * It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list. * when we get close to the start or end of the list.
* *
* Each child element should have a 'data-scroll-token'. This token is used to * Each child element should have a 'data-scroll-tokens'. This string of
* serialise the scroll state, and returned as the 'trackedScrollToken' * comma-separated tokens may contain a single token or many, where many indicates
* attribute by getScrollState(). * that the element contains elements that have scroll tokens themselves. The first
* token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
* as the 'trackedScrollToken' attribute by getScrollState().
*
* IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
* *
* Some notes about the implementation: * Some notes about the implementation:
* *
@ -349,8 +353,8 @@ module.exports = React.createClass({
// Subtract height of tile as if it were unpaginated // Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight; excessHeight -= tile.clientHeight;
// The tile may not have a scroll token, so guard it // The tile may not have a scroll token, so guard it
if (tile.dataset.scrollToken) { if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollToken; markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
} }
if (tile.clientHeight > excessHeight) { if (tile.clientHeight > excessHeight) {
break; break;
@ -419,7 +423,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child. * scroll. false if we are tracking a particular child.
* *
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is * string trackedScrollToken: undefined if stuckAtBottom is true; if it is
* false, the data-scroll-token of the child which we are tracking. * false, the first token in data-scroll-tokens of the child which we are
* tracking.
* *
* number pixelOffset: undefined if stuckAtBottom is true; if it is false, * number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the * the number of pixels the bottom of the tracked child is above the
@ -551,8 +556,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children; var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) { for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i]; var m = messages[i];
if (!m.dataset.scrollToken) continue; // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
if (m.dataset.scrollToken == scrollToken) { // There might only be one scroll token
if (m.dataset.scrollTokens &&
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m; node = m;
break; break;
} }
@ -568,7 +575,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) { if(scrollDelta != 0) {
@ -591,12 +598,12 @@ module.exports = React.createClass({
for (var i = messages.length-1; i >= 0; --i) { for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i]; var node = messages[i];
if (!node.dataset.scrollToken) continue; if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
newScrollState = { newScrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken, trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom, pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}; };
// If the bottom of the panel intersects the ClientRect of node, use this node // If the bottom of the panel intersects the ClientRect of node, use this node
@ -608,7 +615,7 @@ module.exports = React.createClass({
break; break;
} }
} }
// This is only false if there were no nodes with `node.dataset.scrollToken` set. // This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) { if (newScrollState) {
this.scrollState = newScrollState; this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState); debuglog("ScrollPanel: saved scroll state", this.scrollState);

View File

@ -170,7 +170,7 @@ var TimelinePanel = React.createClass({
forwardPaginating: false, forwardPaginating: false,
// cache of matrixClient.getSyncState() (but from the 'sync' event) // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: null, clientSyncState: MatrixClientPeg.get().getSyncState(),
}; };
}, },
@ -503,7 +503,9 @@ var TimelinePanel = React.createClass({
// This happens on user_activity_end which is delayed, and it's // This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check // very possible have logged out within that timeframe, so check
// we still have a client. // we still have a client.
if (!MatrixClientPeg.get()) return; const cli = MatrixClientPeg.get();
// if no client or client is guest don't send RR
if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true); var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
@ -766,6 +768,19 @@ var TimelinePanel = React.createClass({
return null; return null;
}, },
canJumpToReadMarker: function() {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
// read messages and unread messages. We already have a badge count and the bottom
// bar to jump to "live" when we have unread messages.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4.
},
/** /**
* called by the parent component when PageUp/Down/etc is pressed. * called by the parent component when PageUp/Down/etc is pressed.
* *

View File

@ -14,32 +14,40 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); const React = require('react');
var ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
var sdk = require('../../index'); const sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
var PlatformPeg = require("../../PlatformPeg"); const PlatformPeg = require("../../PlatformPeg");
var Modal = require('../../Modal'); const Modal = require('../../Modal');
var dis = require("../../dispatcher"); const dis = require("../../dispatcher");
var q = require('q'); const q = require('q');
var package_json = require('../../../package.json'); const packageJson = require('../../../package.json');
var UserSettingsStore = require('../../UserSettingsStore'); const UserSettingsStore = require('../../UserSettingsStore');
var CallMediaHandler = require('../../CallMediaHandler'); const GeminiScrollbar = require('react-gemini-scrollbar');
var GeminiScrollbar = require('react-gemini-scrollbar'); const Email = require('../../email');
var Email = require('../../email'); const AddThreepid = require('../../AddThreepid');
var AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig');
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. Prepend version with v, to look like riot-web version // 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>'; const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
// Simple method to help prettify GH Release Tags and Commit Hashes. // Simple method to help prettify GH Release Tags and Commit Hashes.
const GHVersionUrl = function(repo, token) { const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; const gHVersionLabel = function(repo, token) {
return `https://github.com/${repo}/${uriTail}`; const match = token.match(semVerRegex);
} let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a href={url}>{token}</a>;
};
// 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
@ -51,7 +59,7 @@ const SETTINGS_LABELS = [
}, },
{ {
id: 'hideReadReceipts', id: 'hideReadReceipts',
label: 'Hide read receipts' label: 'Hide read receipts',
}, },
{ {
id: 'dontSendTypingNotifications', id: 'dontSendTypingNotifications',
@ -107,7 +115,7 @@ const THEMES = [
id: 'theme', id: 'theme',
label: 'Dark theme', label: 'Dark theme',
value: 'dark', value: 'dark',
} },
]; ];
module.exports = React.createClass({ module.exports = React.createClass({
@ -193,7 +201,7 @@ module.exports = React.createClass({
}); });
this._refreshFromServer(); this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings(); const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) { if (!syncedSettings.theme) {
syncedSettings.theme = 'light'; syncedSettings.theme = 'light';
} }
@ -217,16 +225,16 @@ module.exports = React.createClass({
middleOpacity: 1.0, middleOpacity: 1.0,
}); });
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
let cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange); cli.removeListener("RoomMember.membership", this._onInviteStateChange);
} }
}, },
_refreshFromServer: function() { _refreshFromServer: function() {
var self = this; const self = this;
q.all([ q.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids() UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) { ]).done(function(resps) {
self.setState({ self.setState({
avatarUrl: resps[0].avatar_url, avatarUrl: resps[0].avatar_url,
@ -234,7 +242,7 @@ module.exports = React.createClass({
phase: "UserSettings.DISPLAY", phase: "UserSettings.DISPLAY",
}); });
}, function(error) { }, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
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",
@ -251,7 +259,7 @@ module.exports = React.createClass({
onAvatarPickerClick: function(ev) { onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: "Please Register",
description: "Guests can't set avatars. Please register.", description: "Guests can't set avatars. Please register.",
@ -265,8 +273,8 @@ module.exports = React.createClass({
}, },
onAvatarSelected: function(ev) { onAvatarSelected: function(ev) {
var self = this; const self = this;
var changeAvatar = this.refs.changeAvatar; const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) { if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!"); console.error("No ChangeAvatar found to upload image to!");
return; return;
@ -275,9 +283,9 @@ module.exports = React.createClass({
// dunno if the avatar changed, re-check it. // dunno if the avatar changed, re-check it.
self._refreshFromServer(); self._refreshFromServer();
}, function(err) { }, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || ""); // const errMsg = (typeof err === "string") ? err : (err.error || "");
console.error("Failed to set avatar: " + err); console.error("Failed to set avatar: " + err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to set avatar", title: "Failed to set avatar",
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : "Operation failed"),
@ -286,7 +294,7 @@ module.exports = React.createClass({
}, },
onLogoutClicked: function(ev) { onLogoutClicked: function(ev) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Sign out?", title: "Sign out?",
description: description:
@ -301,7 +309,7 @@ module.exports = React.createClass({
<button key="export" 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>,
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
@ -315,34 +323,33 @@ module.exports = React.createClass({
}, },
onPasswordChangeError: function(err) { onPasswordChangeError: function(err) {
var errMsg = err.error || ""; let errMsg = err.error || "";
if (err.httpStatus === 403) { if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?"; errMsg = "Failed to change password. Is your password correct?";
} } else if (err.httpStatus) {
else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`; errMsg += ` (HTTP status ${err.httpStatus})`;
} }
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg); console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: "Error",
description: errMsg description: errMsg,
}); });
}, },
onPasswordChanged: function() { onPasswordChanged: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Success", title: "Success",
description: `Your password was successfully changed. You will not description: `Your password was successfully changed. You will not
receive push notifications on other devices until you receive push notifications on other devices until you
log back in to them.` log back in to them.`,
}); });
}, },
onUpgradeClicked: function() { onUpgradeClicked: function() {
dis.dispatch({ dis.dispatch({
action: "start_upgrade_registration" action: "start_upgrade_registration",
}); });
}, },
@ -356,11 +363,11 @@ module.exports = React.createClass({
}, },
_addEmail: function() { _addEmail: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var email_address = this.refs.add_email_input.value; const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(email_address)) { if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address", title: "Invalid Email Address",
description: "This doesn't appear to be a valid email address", description: "This doesn't appear to be a valid email address",
@ -370,7 +377,7 @@ module.exports = React.createClass({
this._addThreepid = new AddThreepid(); this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the // we always bind emails when registering, so let's do the
// same here. // same here.
this._addThreepid.addEmailAddress(email_address, true).done(() => { this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Verification Pending", title: "Verification Pending",
description: "Please check your email and click on the link it contains. Once this is done, click continue.", description: "Please check your email and click on the link it contains. Once this is done, click continue.",
@ -379,7 +386,7 @@ module.exports = React.createClass({
}); });
}, (err) => { }, (err) => {
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 " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to add email address", title: "Unable to add email address",
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : "Operation failed"),
@ -433,9 +440,9 @@ module.exports = React.createClass({
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
}, (err) => { }, (err) => {
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') { if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var message = "Unable to verify email address. "; let message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue."; message += "Please check your email and click on the link it contains. Once this is done, click continue.";
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Verification Pending", title: "Verification Pending",
@ -444,7 +451,7 @@ module.exports = React.createClass({
onFinished: this.onEmailDialogFinished, onFinished: this.onEmailDialogFinished,
}); });
} else { } else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const 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: "Unable to verify email address", title: "Unable to verify email address",
@ -484,17 +491,17 @@ module.exports = React.createClass({
_onRejectAllInvitesClicked: function(rooms, ev) { _onRejectAllInvitesClicked: function(rooms, ev) {
this.setState({ this.setState({
rejectingInvites: true rejectingInvites: true,
}); });
// reject the invites // reject the invites
let promises = rooms.map((room) => { const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId); return MatrixClientPeg.get().leave(room.roomId);
}); });
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI // purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites. // after trying to reject all the invites.
q.allSettled(promises).then(() => { q.allSettled(promises).then(() => {
this.setState({ this.setState({
rejectingInvites: false rejectingInvites: false,
}); });
}).done(); }).done();
}, },
@ -507,7 +514,7 @@ module.exports = React.createClass({
}, "e2e-export"); }, "e2e-export");
}, { }, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
} },
); );
}, },
@ -519,7 +526,7 @@ module.exports = React.createClass({
}, "e2e-export"); }, "e2e-export");
}, { }, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
} },
); );
}, },
@ -545,8 +552,6 @@ module.exports = React.createClass({
}, },
_renderUserInterfaceSettings: function() { _renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
return ( return (
<div> <div>
<h3>User Interface</h3> <h3>User Interface</h3>
@ -564,7 +569,7 @@ module.exports = React.createClass({
<input id="urlPreviewsDisabled" <input id="urlPreviewsDisabled"
type="checkbox" type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() } defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/> />
<label htmlFor="urlPreviewsDisabled"> <label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default Disable inline URL previews by default
@ -577,7 +582,7 @@ module.exports = React.createClass({
<input id={ setting.id } <input id={ setting.id }
type="checkbox" type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] } defaultChecked={ this._syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ setting.label } { setting.label }
@ -592,7 +597,7 @@ module.exports = React.createClass({
name={ setting.id } name={ setting.id }
value={ setting.value } value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value } defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => { onChange={ (e) => {
if (e.target.checked) { if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value); UserSettingsStore.setSyncedSetting(setting.id, setting.value);
} }
@ -654,8 +659,8 @@ module.exports = React.createClass({
type="checkbox" type="checkbox"
defaultChecked={ this._localSettings[setting.id] } defaultChecked={ this._localSettings[setting.id] }
onChange={ onChange={
e => { (e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked) UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked); client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
} }
@ -669,7 +674,7 @@ module.exports = React.createClass({
}, },
_renderDevicesPanel: function() { _renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel'); const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return ( return (
<div> <div>
<h3>Devices</h3> <h3>Devices</h3>
@ -680,7 +685,7 @@ module.exports = React.createClass({
_renderBugReport: function() { _renderBugReport: function() {
if (!SdkConfig.get().bug_report_endpoint_url) { if (!SdkConfig.get().bug_report_endpoint_url) {
return <div /> return <div />;
} }
return ( return (
<div> <div>
@ -699,17 +704,17 @@ module.exports = React.createClass({
// default to enabled if undefined // default to enabled if undefined
if (this.props.enableLabs === false) return null; if (this.props.enableLabs === false) return null;
let features = UserSettingsStore.LABS_FEATURES.map(feature => ( const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
<div key={feature.id} className="mx_UserSettings_toggle"> <div key={feature.id} className="mx_UserSettings_toggle">
<input <input
type="checkbox" type="checkbox"
id={feature.id} id={feature.id}
name={feature.id} name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) } defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={e => { onChange={(e) => {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
e.target.checked = false; e.target.checked = false;
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: "Please Register",
description: "Guests can't use labs features. Please register.", description: "Guests can't use labs features. Please register.",
@ -761,14 +766,14 @@ module.exports = React.createClass({
}, },
_renderBulkOptions: function() { _renderBulkOptions: function() {
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite"); return r.hasMembershipState(this._me, "invite");
}); });
if (invitedRooms.length === 0) { if (invitedRooms.length === 0) {
return null; return null;
} }
let Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
let reject = <Spinner />; let reject = <Spinner />;
if (!this.state.rejectingInvites) { if (!this.state.rejectingInvites) {
@ -852,9 +857,7 @@ module.exports = React.createClass({
_showSpoiler: function(event) { _showSpoiler: function(event) {
const target = event.target; const target = event.target;
const hidden = target.getAttribute('data-spoiler'); target.innerHTML = target.getAttribute('data-spoiler');
target.innerHTML = hidden;
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(target); range.selectNodeContents(target);
@ -865,12 +868,12 @@ module.exports = React.createClass({
}, },
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);
}, },
presentableTextForThreepid: function(threepid) { presentableTextForThreepid: function(threepid) {
if (threepid.medium == 'msisdn') { if (threepid.medium === 'msisdn') {
return '+' + threepid.address; return '+' + threepid.address;
} else { } else {
return threepid.address; return threepid.address;
@ -878,7 +881,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) { switch (this.state.phase) {
case "UserSettings.LOADING": case "UserSettings.LOADING":
return ( return (
@ -890,18 +893,18 @@ module.exports = React.createClass({
throw new Error("Unknown state.phase => " + this.state.phase); throw new Error("Unknown state.phase => " + this.state.phase);
} }
// can only get here if phase is UserSettings.DISPLAY // can only get here if phase is UserSettings.DISPLAY
var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName"); const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
var ChangePassword = sdk.getComponent("views.settings.ChangePassword"); const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications"); const Notifications = sdk.getComponent("settings.Notifications");
var EditableText = sdk.getComponent('elements.EditableText'); const EditableText = sdk.getComponent('elements.EditableText');
var avatarUrl = ( const avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
); );
var threepidsSection = this.state.threepids.map((val, pidIndex) => { const threepidsSection = this.state.threepids.map((val, pidIndex) => {
const id = "3pid-" + val.address; const id = "3pid-" + val.address;
return ( return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}> <div className="mx_UserSettings_profileTableRow" key={pidIndex}>
@ -949,7 +952,7 @@ module.exports = React.createClass({
threepidsSection.push(addEmailSection); threepidsSection.push(addEmailSection);
threepidsSection.push(addMsisdnSection); threepidsSection.push(addMsisdnSection);
var accountJsx; let accountJsx;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
accountJsx = ( accountJsx = (
@ -957,8 +960,7 @@ module.exports = React.createClass({
Create an account Create an account
</div> </div>
); );
} } else {
else {
accountJsx = ( accountJsx = (
<ChangePassword <ChangePassword
className="mx_UserSettings_accountTable" className="mx_UserSettings_accountTable"
@ -970,9 +972,9 @@ module.exports = React.createClass({
onFinished={this.onPasswordChanged} /> onFinished={this.onPasswordChanged} />
); );
} }
var notification_area; let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) { if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notification_area = (<div> notificationArea = (<div>
<h3>Notifications</h3> <h3>Notifications</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
@ -986,7 +988,7 @@ module.exports = React.createClass({
// 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.
let olmVersionString = "<not-enabled>"; let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) { if (olmVersion !== undefined) {
olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
} }
return ( return (
@ -1044,7 +1046,7 @@ module.exports = React.createClass({
{this._renderReferral()} {this._renderReferral()}
{notification_area} {notificationArea}
{this._renderUserInterfaceSettings()} {this._renderUserInterfaceSettings()}
{this._renderLabs()} {this._renderLabs()}
@ -1061,7 +1063,10 @@ module.exports = React.createClass({
Logged in as {this._me} Logged in as {this._me}
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Access Token: <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;click to reveal&gt;</span> Access Token: <span className="mx_UserSettings_advanced_spoiler"
onClick={this._showSpoiler}
data-spoiler={ MatrixClientPeg.get().getAccessToken() }
>&lt;click to reveal&gt;</span>
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() } Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
@ -1071,11 +1076,11 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>') matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
? <a href={ GHVersionUrl('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) }>{REACT_SDK_VERSION}</a> ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION : REACT_SDK_VERSION
}<br/> }<br/>
riot-web version: {(this.state.vectorVersion !== null) riot-web version: {(this.state.vectorVersion !== null)
? <a href={ GHVersionUrl('vector-im/riot-web', this.state.vectorVersion.split('-')[0]) }>{this.state.vectorVersion}</a> ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown' : 'unknown'
}<br/> }<br/>
olm version: {olmVersionString}<br/> olm version: {olmVersionString}<br/>
@ -1089,5 +1094,5 @@ module.exports = React.createClass({
</GeminiScrollbar> </GeminiScrollbar>
</div> </div>
); );
} },
}); });

View File

@ -23,6 +23,9 @@ import url from 'url';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
/** /**
* A wire component which glues together login UI components and Login logic * A wire component which glues together login UI components and Login logic
*/ */
@ -125,7 +128,16 @@ module.exports = React.createClass({
}, },
onPhoneNumberChanged: function(phoneNumber) { onPhoneNumberChanged: function(phoneNumber) {
this.setState({ phoneNumber: phoneNumber }); // Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: 'The phone number entered looks invalid' });
return;
}
this.setState({
phoneNumber: phoneNumber,
errorText: null,
});
}, },
onServerConfigChange: function(config) { onServerConfigChange: function(config) {

View File

@ -59,7 +59,9 @@ module.exports = React.createClass({
ContentRepo.getHttpUriForMxc( ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl, props.oobData.avatarUrl,
props.width, props.height, props.resizeMethod Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod
), // highest priority ), // highest priority
this.getRoomAvatarUrl(props), this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), this.getOneToOneAvatar(props),
@ -74,7 +76,9 @@ module.exports = React.createClass({
return props.room.getAvatarUrl( return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false false
); );
}, },
@ -103,14 +107,18 @@ module.exports = React.createClass({
} }
return theOtherGuy.getAvatarUrl( return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false false
); );
} else if (userIds.length == 1) { } else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl( return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
false Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
); );
} else { } else {
return null; return null;

View File

@ -369,6 +369,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const eventsToRender = this.props.events; const eventsToRender = this.props.events;
const eventIds = eventsToRender.map(e => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold; const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents; const expanded = this.state.expanded || fewEvents;
@ -379,7 +380,7 @@ module.exports = React.createClass({
if (fewEvents) { if (fewEvents) {
return ( return (
<div className="mx_MemberEventListSummary"> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents} {expandedEvents}
</div> </div>
); );
@ -437,7 +438,7 @@ module.exports = React.createClass({
); );
return ( return (
<div className="mx_MemberEventListSummary"> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{toggleButton} {toggleButton}
{summaryContainer} {summaryContainer}
{expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null} {expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}

View File

@ -97,7 +97,7 @@ module.exports = React.createClass({
if (this.props.selectedRoom) { if (this.props.selectedRoom) {
constantTimeDispatcher.dispatch( constantTimeDispatcher.dispatch(
"RoomTile.select", this.props.selectedRoom, {} "RoomTile.select", this.props.selectedRoom, {}
); );
} }
constantTimeDispatcher.dispatch( constantTimeDispatcher.dispatch(
"RoomTile.select", nextProps.selectedRoom, { selected: true } "RoomTile.select", nextProps.selectedRoom, { selected: true }
@ -265,7 +265,7 @@ module.exports = React.createClass({
}, },
onRoomStateMember: function(ev, state, member) { onRoomStateMember: function(ev, state, member) {
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
ev.getPrevContent() && ev.getPrevContent().membership === "invite") ev.getPrevContent() && ev.getPrevContent().membership === "invite")
{ {
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
@ -290,7 +290,7 @@ module.exports = React.createClass({
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
} }
else if (ev.getType() == 'm.push_rules') { else if (ev.getType() == 'm.push_rules') {
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
} }
}, },
@ -318,7 +318,7 @@ module.exports = React.createClass({
// as needed. // as needed.
// Alternatively we'd do something magical with Immutable.js or similar. // Alternatively we'd do something magical with Immutable.js or similar.
this.setState(this.getRoomLists()); this.setState(this.getRoomLists());
// this._lastRefreshRoomListTs = Date.now(); // this._lastRefreshRoomListTs = Date.now();
}, },
@ -341,7 +341,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().getRooms().forEach(function(room) { MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId); const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return; if (!me) return;
// console.log("room = " + room.name + ", me.membership = " + me.membership + // console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() + // ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() + // ", target = " + me.events.member.getStateKey() +
@ -391,51 +391,10 @@ module.exports = React.createClass({
} }
}); });
if (s.lists["im.vector.fake.direct"].length == 0 &&
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
!MatrixClientPeg.get().isGuest())
{
// scan through the 'recents' list for any rooms which look like DM rooms
// and make them DM rooms
const oldRecents = s.lists["im.vector.fake.recent"];
s.lists["im.vector.fake.recent"] = [];
for (const room of oldRecents) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
} else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
// save these new guessed DM rooms into the account data
const newMDirectEvent = {};
for (const room of s.lists["im.vector.fake.direct"]) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const otherPerson = Rooms.getOnlyOtherMember(room, me);
if (!otherPerson) continue;
const roomList = newMDirectEvent[otherPerson.userId] || [];
roomList.push(room.roomId);
newMDirectEvent[otherPerson.userId] = roomList;
}
console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
// if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
}
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
// we actually apply the sorting to this when receiving the prop in RoomSubLists. // we actually apply the sorting to this when receiving the prop in RoomSubLists.
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/* /*
this.listOrder = [ this.listOrder = [
"im.vector.fake.invite", "im.vector.fake.invite",
"m.favourite", "m.favourite",

View File

@ -60,7 +60,7 @@ module.exports = React.createClass({
} }
} }
return ( return (
<li data-scroll-token={eventId+"+"+j}> <li data-scroll-tokens={eventId+"+"+j}>
{ret} {ret}
</li>); </li>);
}, },

View File

@ -19,6 +19,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
// cancel button which is shared between room header and simple room header // cancel button which is shared between room header and simple room header
export function CancelButton(props) { export function CancelButton(props) {
@ -45,6 +46,9 @@ export default React.createClass({
// is the RightPanel collapsed? // is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool,
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
}, },
onShowRhsClick: function(ev) { onShowRhsClick: function(ev) {
@ -53,9 +57,17 @@ export default React.createClass({
render: function() { render: function() {
let cancelButton; let cancelButton;
let icon;
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />; cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
} }
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
/>;
}
let showRhsButton; let showRhsButton;
/* // don't bother cluttering things up with this for now. /* // don't bother cluttering things up with this for now.
@ -73,6 +85,7 @@ export default React.createClass({
<div className="mx_RoomHeader" > <div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title } { this.props.title }
{ showRhsButton } { showRhsButton }
{ cancelButton } { cancelButton }

View File

@ -115,7 +115,7 @@ var Tester = React.createClass({
// //
// there is an extra 50 pixels of margin at the bottom. // there is an extra 50 pixels of margin at the bottom.
return ( return (
<li key={key} data-scroll-token={key}> <li key={key} data-scroll-tokens={key}>
<div style={{height: '98px', margin: '50px', border: '1px solid black', <div style={{height: '98px', margin: '50px', border: '1px solid black',
backgroundColor: '#fff8dc' }}> backgroundColor: '#fff8dc' }}>
{key} {key}