Merge branch 'develop' into wmwragg/direct-chat-sublist

This commit is contained in:
wmwragg 2016-08-23 10:52:50 +01:00
commit 6d1f9003e2
24 changed files with 593 additions and 195 deletions

View file

@ -1,3 +1,224 @@
Changes in [0.6.4-r1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.4-r1) (2016-08-12)
=========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4...v0.6.4-r1)
* Fix inviting multiple people
Changes in [0.6.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.4) (2016-08-11)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.3...v0.6.4)
* Only show Autocomplete if autocomplete is enabled
[\#411](https://github.com/matrix-org/matrix-react-sdk/pull/411)
* Wmwragg/room tag menu
[\#402](https://github.com/matrix-org/matrix-react-sdk/pull/402)
* Move guest registration into the login logic
[\#407](https://github.com/matrix-org/matrix-react-sdk/pull/407)
* Better support for inviting multiple people
[\#403](https://github.com/matrix-org/matrix-react-sdk/pull/403)
* Refactor login token
[\#406](https://github.com/matrix-org/matrix-react-sdk/pull/406)
* Use the current HS for guest login
[\#405](https://github.com/matrix-org/matrix-react-sdk/pull/405)
* Various fixes and improvements to emojification.
[\#395](https://github.com/matrix-org/matrix-react-sdk/pull/395)
* Fix settings resetting on refresh
[\#404](https://github.com/matrix-org/matrix-react-sdk/pull/404)
* Avoid flashing up login screen during guest registration
[\#401](https://github.com/matrix-org/matrix-react-sdk/pull/401)
* Cancel calls to rate-limited funcs on unmount
[\#400](https://github.com/matrix-org/matrix-react-sdk/pull/400)
* Move rehydration of MatrixClients from MatrixClientPeg to SessionLoader
[\#399](https://github.com/matrix-org/matrix-react-sdk/pull/399)
* Don't show integrations header if setting not on
[\#398](https://github.com/matrix-org/matrix-react-sdk/pull/398)
* Start to factor out session-loading magic
[\#397](https://github.com/matrix-org/matrix-react-sdk/pull/397)
* Hack around a react warning
[\#396](https://github.com/matrix-org/matrix-react-sdk/pull/396)
* Add config to hide the labs section
[\#393](https://github.com/matrix-org/matrix-react-sdk/pull/393)
* Dbkr/scalar
[\#392](https://github.com/matrix-org/matrix-react-sdk/pull/392)
* Wmwragg/mute mention state fix
[\#390](https://github.com/matrix-org/matrix-react-sdk/pull/390)
* Fix long freeze when opening 'historical' section
[\#391](https://github.com/matrix-org/matrix-react-sdk/pull/391)
* Refactor UI error effects
[\#388](https://github.com/matrix-org/matrix-react-sdk/pull/388)
* Implement account deactivation
[\#381](https://github.com/matrix-org/matrix-react-sdk/pull/381)
* Don't leave isRoomPublished as undefined
[\#389](https://github.com/matrix-org/matrix-react-sdk/pull/389)
* Call the logout API when we log out
[\#377](https://github.com/matrix-org/matrix-react-sdk/pull/377)
* feat: code cleanup & emoji replacement in composer
[\#335](https://github.com/matrix-org/matrix-react-sdk/pull/335)
* Add more logging to TimelinePanel-test
[\#387](https://github.com/matrix-org/matrix-react-sdk/pull/387)
* DevicesPanel: use device_id as a placeholder
[\#386](https://github.com/matrix-org/matrix-react-sdk/pull/386)
* MemberDeviceInfo: Use the device name, where available
[\#385](https://github.com/matrix-org/matrix-react-sdk/pull/385)
* Wmwragg/mention state menu
[\#369](https://github.com/matrix-org/matrix-react-sdk/pull/369)
* fix upload for video or image files where sniffing fails
[\#383](https://github.com/matrix-org/matrix-react-sdk/pull/383)
* fix: allow up/down normally for no completions
[\#384](https://github.com/matrix-org/matrix-react-sdk/pull/384)
* fix: autocomplete to use tab instead of return
[\#382](https://github.com/matrix-org/matrix-react-sdk/pull/382)
* strip (IRC) displayname suffix from autocomplete
[\#375](https://github.com/matrix-org/matrix-react-sdk/pull/375)
* Include rooms with 1 person invited
[\#379](https://github.com/matrix-org/matrix-react-sdk/pull/379)
* Fix 'start new direct chat'
[\#378](https://github.com/matrix-org/matrix-react-sdk/pull/378)
* Fix warnings from MessageComposer
[\#376](https://github.com/matrix-org/matrix-react-sdk/pull/376)
* New voice and video call buttons
[\#371](https://github.com/matrix-org/matrix-react-sdk/pull/371)
* Silence some more react warnings
[\#373](https://github.com/matrix-org/matrix-react-sdk/pull/373)
* Fix warnings emanating from Velociraptor elements
[\#372](https://github.com/matrix-org/matrix-react-sdk/pull/372)
* Wmwragg/button updates
[\#353](https://github.com/matrix-org/matrix-react-sdk/pull/353)
* Implement device management UI
[\#370](https://github.com/matrix-org/matrix-react-sdk/pull/370)
* Factor EditableTextContainer out of ChangeDisplayName
[\#368](https://github.com/matrix-org/matrix-react-sdk/pull/368)
* Stop the Avatar classes setting properties on <span>s
[\#367](https://github.com/matrix-org/matrix-react-sdk/pull/367)
* Remove relayoutOnUpdate prop on gemini-scrollbar
[\#366](https://github.com/matrix-org/matrix-react-sdk/pull/366)
* Fix bug where vector freezes on power level event
[\#364](https://github.com/matrix-org/matrix-react-sdk/pull/364)
* Refactor MatrixClientPeg
[\#361](https://github.com/matrix-org/matrix-react-sdk/pull/361)
* Fix 'start chat' button on MemberInfo
[\#363](https://github.com/matrix-org/matrix-react-sdk/pull/363)
* Bump dependency versions
[\#362](https://github.com/matrix-org/matrix-react-sdk/pull/362)
* Fix tab complete order properly
[\#360](https://github.com/matrix-org/matrix-react-sdk/pull/360)
* Add removeListener for account data listener
[\#359](https://github.com/matrix-org/matrix-react-sdk/pull/359)
* Set the device_id on pre-login MatrixClient
[\#358](https://github.com/matrix-org/matrix-react-sdk/pull/358)
* Wmwragg/mention state indicator round 2
[\#357](https://github.com/matrix-org/matrix-react-sdk/pull/357)
* Support for disabling/enabling URL previews per-user, per-room and per-user-
per-room
[\#356](https://github.com/matrix-org/matrix-react-sdk/pull/356)
* Use HS proxy API for requestToken on adding email
[\#336](https://github.com/matrix-org/matrix-react-sdk/pull/336)
* Error if email already in use when resetting pw
[\#337](https://github.com/matrix-org/matrix-react-sdk/pull/337)
* Fix enourmous video bug
[\#355](https://github.com/matrix-org/matrix-react-sdk/pull/355)
* Add support for sending uploaded content as m.video
[\#354](https://github.com/matrix-org/matrix-react-sdk/pull/354)
* Order tab complete by most recently spoke
[\#341](https://github.com/matrix-org/matrix-react-sdk/pull/341)
* Wmwragg/spinner fix
[\#350](https://github.com/matrix-org/matrix-react-sdk/pull/350)
* Now showing three dots when hovering over the badge
[\#352](https://github.com/matrix-org/matrix-react-sdk/pull/352)
* Fix unpublishing room in room settings
[\#351](https://github.com/matrix-org/matrix-react-sdk/pull/351)
* Fix race when creating rooms where invite list can be blank
[\#347](https://github.com/matrix-org/matrix-react-sdk/pull/347)
* improve wording of MemberInfo's start chat button.
[\#348](https://github.com/matrix-org/matrix-react-sdk/pull/348)
* Revert "Amends react template and removes opening image in lightbox"
[\#346](https://github.com/matrix-org/matrix-react-sdk/pull/346)
* Wmwragg/modal restyle
[\#345](https://github.com/matrix-org/matrix-react-sdk/pull/345)
* Amends react template and removes opening image in lightbox
[\#343](https://github.com/matrix-org/matrix-react-sdk/pull/343)
* Remove the member list loading hack
[\#344](https://github.com/matrix-org/matrix-react-sdk/pull/344)
* CSS classes to colour offline users differently
[\#342](https://github.com/matrix-org/matrix-react-sdk/pull/342)
* Listen for the new lastPreseceTs event
[\#340](https://github.com/matrix-org/matrix-react-sdk/pull/340)
* Fix filtering user list by ID
[\#339](https://github.com/matrix-org/matrix-react-sdk/pull/339)
* Update tab completion list when we have a room
[\#338](https://github.com/matrix-org/matrix-react-sdk/pull/338)
* JS code style guide
[\#330](https://github.com/matrix-org/matrix-react-sdk/pull/330)
* Error on registration if email taken
[\#334](https://github.com/matrix-org/matrix-react-sdk/pull/334)
* feat: render unicode emoji as emojione images
[\#332](https://github.com/matrix-org/matrix-react-sdk/pull/332)
* feat: unblacklist img tags with data URIs
[\#333](https://github.com/matrix-org/matrix-react-sdk/pull/333)
* Autocomplete fixes
[\#331](https://github.com/matrix-org/matrix-react-sdk/pull/331)
* Better autocomplete
[\#296](https://github.com/matrix-org/matrix-react-sdk/pull/296)
* feat: add and configure eslint
[\#329](https://github.com/matrix-org/matrix-react-sdk/pull/329)
* Fix user links
[\#326](https://github.com/matrix-org/matrix-react-sdk/pull/326)
* Fix ordering of Memberlist
[\#327](https://github.com/matrix-org/matrix-react-sdk/pull/327)
* Display an error message if room not found
[\#325](https://github.com/matrix-org/matrix-react-sdk/pull/325)
* Implement device blocking
[\#324](https://github.com/matrix-org/matrix-react-sdk/pull/324)
* Remove /encrypt command
[\#322](https://github.com/matrix-org/matrix-react-sdk/pull/322)
* RoomSettings: add encryption setting
[\#321](https://github.com/matrix-org/matrix-react-sdk/pull/321)
* Fix a pair of warnings from RoomSettings
[\#320](https://github.com/matrix-org/matrix-react-sdk/pull/320)
* RoomSettings: refactor permissions calculations
[\#319](https://github.com/matrix-org/matrix-react-sdk/pull/319)
* Fix https://github.com/vector-im/vector-web/issues/1679
[\#318](https://github.com/matrix-org/matrix-react-sdk/pull/318)
* Fix /join to be consistent with the other code
[\#317](https://github.com/matrix-org/matrix-react-sdk/pull/317)
* UserSettings: fix the displayed version of the react-sdk
[\#316](https://github.com/matrix-org/matrix-react-sdk/pull/316)
* Show canonical alias in URL bar
[\#314](https://github.com/matrix-org/matrix-react-sdk/pull/314)
* Some basic tests for RoomView
[\#313](https://github.com/matrix-org/matrix-react-sdk/pull/313)
* Support for making devices unverified
[\#315](https://github.com/matrix-org/matrix-react-sdk/pull/315)
* Fix eventListener warning
[\#312](https://github.com/matrix-org/matrix-react-sdk/pull/312)
* Fix peeking and member list vanishing
[\#307](https://github.com/matrix-org/matrix-react-sdk/pull/307)
* Use different keys for new MessageComposerInput
[\#311](https://github.com/matrix-org/matrix-react-sdk/pull/311)
* Fix RTE escaping, HTML output with breaks
[\#310](https://github.com/matrix-org/matrix-react-sdk/pull/310)
* Fix cursor bug, persist editor mode & rte default
[\#308](https://github.com/matrix-org/matrix-react-sdk/pull/308)
* Rich Text Editor
[\#292](https://github.com/matrix-org/matrix-react-sdk/pull/292)
* Hide e2e features if not enabled
[\#306](https://github.com/matrix-org/matrix-react-sdk/pull/306)
* Add experimental "Labs" section to settings
[\#305](https://github.com/matrix-org/matrix-react-sdk/pull/305)
* Make the room directory join rooms by alias
[\#304](https://github.com/matrix-org/matrix-react-sdk/pull/304)
* Factor out common parts of room creation
[\#303](https://github.com/matrix-org/matrix-react-sdk/pull/303)
* Fix spinner-of-doom in member info for guests
[\#302](https://github.com/matrix-org/matrix-react-sdk/pull/302)
* Support for marking devices as verified
[\#300](https://github.com/matrix-org/matrix-react-sdk/pull/300)
* Make the config optional
[\#301](https://github.com/matrix-org/matrix-react-sdk/pull/301)
* Pass brand parameter down to Notifications
[\#299](https://github.com/matrix-org/matrix-react-sdk/pull/299)
* Second attempt at fixing the Velocity memory leak
[\#298](https://github.com/matrix-org/matrix-react-sdk/pull/298)
Changes in [0.6.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.3) (2016-06-03) Changes in [0.6.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.3) (2016-06-03)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.2...v0.6.3) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.2...v0.6.3)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.6.3", "version": "0.6.4-r1",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -40,7 +40,7 @@
"linkifyjs": "^2.0.0-beta.4", "linkifyjs": "^2.0.0-beta.4",
"lodash": "^4.13.1", "lodash": "^4.13.1",
"marked": "^0.3.5", "marked": "^0.3.5",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "0.5.5",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^15.2.1", "react": "^15.2.1",

View file

@ -273,8 +273,11 @@ function _onAction(payload) {
break; break;
case 'incoming_call': case 'incoming_call':
if (module.exports.getAnyActiveCall()) { if (module.exports.getAnyActiveCall()) {
payload.call.hangup("busy"); // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
return; // don't allow >1 call to be received, hangup newer one. // we avoid rejecting with "busy" in case the user wants to answer it on a different device.
// in future we could signal a "local busy" as a warning to the caller.
// see https://github.com/vector-im/vector-web/issues/1964
return;
} }
// if the runtime env doesn't do VoIP, stop here. // if the runtime env doesn't do VoIP, stop here.

View file

@ -69,7 +69,7 @@ var sanitizeHtmlParams = {
allowedAttributes: { allowedAttributes: {
// custom ones first: // custom ones first:
font: [ 'color' ], // custom to matrix font: [ 'color' ], // custom to matrix
a: [ 'href', 'name', 'target' ], // remote target: custom to matrix a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix
// We don't currently allow img itself by default, but this // We don't currently allow img itself by default, but this
// would make sense if we did // would make sense if we did
img: [ 'src' ], img: [ 'src' ],
@ -92,6 +92,7 @@ var sanitizeHtmlParams = {
else { else {
attribs.target = '_blank'; attribs.target = '_blank';
} }
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs }; return { tagName: tagName, attribs : attribs };
}, },
}, },

View file

@ -69,6 +69,7 @@ export function loadSession(opts) {
let enableGuest = opts.enableGuest || false; let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl; const guestHsUrl = opts.guestHsUrl;
const guestIsUrl = opts.guestIsUrl; const guestIsUrl = opts.guestIsUrl;
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) { if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) {
// this happens during email validation: the email contains a link to the // this happens during email validation: the email contains a link to the
@ -87,7 +88,7 @@ export function loadSession(opts) {
if (!realQueryParams.homeserver) { if (!realQueryParams.homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use"); console.warn("Cannot log in with token: can't determine HS URL to use");
} else { } else {
return _loginWithToken(realQueryParams); return _loginWithToken(realQueryParams, defaultDeviceDisplayName);
} }
} }
@ -111,23 +112,29 @@ export function loadSession(opts) {
} }
if (enableGuest) { if (enableGuest) {
return _registerAsGuest(guestHsUrl, guestIsUrl); return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
} }
// fall back to login screen // fall back to login screen
return q(); return q();
} }
function _loginWithToken(queryParams) { function _loginWithToken(queryParams, defaultDeviceDisplayName) {
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
var client = Matrix.createClient({ var client = Matrix.createClient({
baseUrl: queryParams.homeserver, baseUrl: queryParams.homeserver,
}); });
return client.loginWithToken(queryParams.loginToken).then(function(data) { return client.login(
"m.login.token", {
token: queryParams.loginToken,
initial_device_display_name: defaultDeviceDisplayName,
},
).then(function(data) {
console.log("Logged in with token"); console.log("Logged in with token");
setLoggedIn({ setLoggedIn({
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token, accessToken: data.access_token,
homeserverUrl: queryParams.homeserver, homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer, identityServerUrl: queryParams.identityServer,
@ -139,14 +146,26 @@ function _loginWithToken(queryParams) {
}); });
} }
function _registerAsGuest(hsUrl, isUrl) { function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
console.log("Doing guest login on %s", hsUrl); console.log("Doing guest login on %s", hsUrl);
MatrixClientPeg.replaceUsingUrls(hsUrl, isUrl); // TODO: we should probably de-duplicate this and Signup.Login.loginAsGuest.
return MatrixClientPeg.get().registerGuest().then((creds) => { // Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login
var client = Matrix.createClient({
baseUrl: hsUrl,
});
return client.registerGuest({
body: {
initial_device_display_name: defaultDeviceDisplayName,
},
}).then((creds) => {
console.log("Registered as guest: %s", creds.user_id); console.log("Registered as guest: %s", creds.user_id);
setLoggedIn({ setLoggedIn({
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id,
accessToken: creds.access_token, accessToken: creds.access_token,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
@ -166,6 +185,7 @@ function _restoreFromLocalStorage() {
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
const access_token = localStorage.getItem("mx_access_token"); const access_token = localStorage.getItem("mx_access_token");
const user_id = localStorage.getItem("mx_user_id"); const user_id = localStorage.getItem("mx_user_id");
const device_id = localStorage.getItem("mx_device_id");
let is_guest; let is_guest;
if (localStorage.getItem("mx_is_guest") !== null) { if (localStorage.getItem("mx_is_guest") !== null) {
@ -179,6 +199,7 @@ function _restoreFromLocalStorage() {
console.log("Restoring session for %s", user_id); console.log("Restoring session for %s", user_id);
setLoggedIn({ setLoggedIn({
userId: user_id, userId: user_id,
deviceId: device_id,
accessToken: access_token, accessToken: access_token,
homeserverUrl: hs_url, homeserverUrl: hs_url,
identityServerUrl: is_url, identityServerUrl: is_url,
@ -206,10 +227,19 @@ export function setLoggedIn(credentials) {
try { try {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl); localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
localStorage.setItem("mx_is_url", credentials.identityServerUrl); localStorage.setItem("mx_is_url", credentials.identityServerUrl);
localStorage.setItem("mx_user_id", credentials.userId); localStorage.setItem("mx_user_id", credentials.userId);
localStorage.setItem("mx_access_token", credentials.accessToken); localStorage.setItem("mx_access_token", credentials.accessToken);
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
// if we didn't get a deviceId from the login, leave mx_device_id unset,
// rather than setting it to "undefined".
//
// (in this case MatrixClient doesn't bother with the crypto stuff
// - that's fine for us).
if (credentials.deviceId) {
localStorage.setItem("mx_device_id", credentials.deviceId);
}
console.log("Session persisted for %s", credentials.userId); console.log("Session persisted for %s", credentials.userId);
} catch (e) { } catch (e) {
console.warn("Error using local storage: can't persist session!", e); console.warn("Error using local storage: can't persist session!", e);
@ -286,7 +316,7 @@ export function onLoggedOut() {
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
} }
_stopMatrixClient(); stopMatrixClient();
dis.dispatch({action: 'on_logged_out'}); dis.dispatch({action: 'on_logged_out'});
} }
@ -294,11 +324,14 @@ export function onLoggedOut() {
/** /**
* Stop all the background processes related to the current client * Stop all the background processes related to the current client
*/ */
function _stopMatrixClient() { export function stopMatrixClient() {
Notifier.stop(); Notifier.stop();
UserActivity.stop(); UserActivity.stop();
Presence.stop(); Presence.stop();
MatrixClientPeg.get().stopClient(); var cli = MatrixClientPeg.get();
MatrixClientPeg.get().removeAllListeners(); if (cli) {
MatrixClientPeg.unset(); cli.stopClient();
cli.removeAllListeners();
MatrixClientPeg.unset();
}
} }

View file

@ -21,21 +21,11 @@ import utils from 'matrix-js-sdk/lib/utils';
const localStorage = window.localStorage; const localStorage = window.localStorage;
function deviceId() {
// XXX: is Math.random()'s deterministicity a problem here?
var id = Math.floor(Math.random()*16777215).toString(16);
id = "W" + "000000".substring(id.length) + id;
if (localStorage) {
id = localStorage.getItem("mx_device_id") || id;
localStorage.setItem("mx_device_id", id);
}
return id;
}
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
identityServerUrl: string, identityServerUrl: string,
userId: string, userId: string,
deviceId: string,
accessToken: string, accessToken: string,
guest: boolean, guest: boolean,
} }
@ -67,26 +57,12 @@ class MatrixClientPeg {
this.matrixClient = null; this.matrixClient = null;
} }
/**
* Replace this MatrixClientPeg's client with a client instance that has
* Home Server / Identity Server URLs but no credentials
*/
replaceUsingUrls(hs_url, is_url) {
this._replaceClient(hs_url, is_url);
}
/** /**
* Replace this MatrixClientPeg's client with a client instance that has * Replace this MatrixClientPeg's client with a client instance that has
* Home Server / Identity Server URLs and active credentials * Home Server / Identity Server URLs and active credentials
*/ */
replaceUsingCreds(creds: MatrixClientCreds) { replaceUsingCreds(creds: MatrixClientCreds) {
this._replaceClient( this._createClient(creds);
creds.homeserverUrl,
creds.identityServerUrl,
creds.userId,
creds.accessToken,
creds.guest,
);
} }
start() { start() {
@ -96,32 +72,29 @@ class MatrixClientPeg {
this.get().startClient(opts); this.get().startClient(opts);
} }
_replaceClient(hs_url, is_url, user_id, access_token, isGuest) {
this._createClient(hs_url, is_url, user_id, access_token, isGuest);
}
getCredentials(): MatrixClientCreds { getCredentials(): MatrixClientCreds {
return { return {
homeserverUrl: this.matrixClient.baseUrl, homeserverUrl: this.matrixClient.baseUrl,
identityServerUrl: this.matrixClient.idBaseUrl, identityServerUrl: this.matrixClient.idBaseUrl,
userId: this.matrixClient.credentials.userId, userId: this.matrixClient.credentials.userId,
deviceId: this.matrixClient.getDeviceId(),
accessToken: this.matrixClient.getAccessToken(), accessToken: this.matrixClient.getAccessToken(),
guest: this.matrixClient.isGuest(), guest: this.matrixClient.isGuest(),
}; };
} }
_createClient(hs_url, is_url, user_id, access_token, isGuest) { _createClient(creds: MatrixClientCreds) {
var opts = { var opts = {
baseUrl: hs_url, baseUrl: creds.homeserverUrl,
idBaseUrl: is_url, idBaseUrl: creds.identityServerUrl,
accessToken: access_token, accessToken: creds.accessToken,
userId: user_id, userId: creds.userId,
deviceId: creds.deviceId,
timelineSupport: true, timelineSupport: true,
}; };
if (localStorage) { if (localStorage) {
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
opts.deviceId = deviceId();
} }
this.matrixClient = Matrix.createClient(opts); this.matrixClient = Matrix.createClient(opts);
@ -130,7 +103,7 @@ class MatrixClientPeg {
// potential number of event listeners is quite high. // potential number of event listeners is quite high.
this.matrixClient.setMaxListeners(500); this.matrixClient.setMaxListeners(500);
this.matrixClient.setGuest(Boolean(isGuest)); this.matrixClient.setGuest(Boolean(creds.guest));
} }
} }

164
src/RoomNotifs.js Normal file
View file

@ -0,0 +1,164 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
import q from 'q';
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
export const ALL_MESSAGES = 'all_messages';
export const MENTIONS_ONLY = 'mentions_only';
export const MUTE = 'mute';
export function getRoomNotifsState(roomId) {
if (MatrixClientPeg.get().isGuest()) return RoomNotifs.ALL_MESSAGES;
// look through the override rules for a rule affecting this room:
// if one exists, it will take precedence.
const muteRule = findOverrideMuteRule(roomId);
if (muteRule) {
return MUTE;
}
// for everything else, look at the room rule.
const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
// XXX: We have to assume the default is to notify for all messages
// (in particular this will be 'wrong' for one to one rooms because
// they will notify loudly for all messages)
if (!roomRule || !roomRule.enabled) return ALL_MESSAGES;
// a mute at the room level will still allow mentions
// to notify
if (isMuteRule(roomRule)) return MENTIONS_ONLY;
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD;
return null;
}
export function setRoomNotifsState(roomId, newState) {
if (newState == MUTE) {
return setRoomNotifsStateMuted(roomId);
} else {
return setRoomNotifsStateUnmuted(roomId, newState);
}
}
function setRoomNotifsStateMuted(roomId) {
const cli = MatrixClientPeg.get();
const promises = [];
// delete the room rule
const roomRule = cli.getRoomPushRule('global', roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
}
// add/replace an override rule to squelch everything in this room
// NB. We use the room ID as the name of this rule too, although this
// is an override rule, not a room rule: it still pertains to this room
// though, so using the room ID as the rule ID is logical and prevents
// duplicate copies of the rule.
promises.push(cli.addPushRule('global', 'override', roomId, {
conditions: [
{
kind: 'event_match',
key: 'room_id',
pattern: roomId,
}
],
actions: [
'dont_notify',
]
}));
return q.all(promises);
}
function setRoomNotifsStateUnmuted(roomId, newState) {
const cli = MatrixClientPeg.get();
const promises = [];
const overrideMuteRule = findOverrideMuteRule(roomId);
if (overrideMuteRule) {
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
}
if (newState == 'all_messages') {
const roomRule = cli.getRoomPushRule('global', roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
}
} else if (newState == 'mentions_only') {
promises.push(cli.addPushRule('global', 'room', roomId, {
actions: [
'dont_notify',
]
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
} else if ('all_messages_loud') {
promises.push(cli.addPushRule('global', 'room', roomId, {
actions: [
'notify',
{
set_tweak: 'sound',
value: 'default',
}
]
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
}
return q.all(promises);
}
function findOverrideMuteRule(roomId) {
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
if (isRuleForRoom(roomId, rule)) {
if (isMuteRule(rule) && rule.enabled) {
return rule;
}
}
}
return null;
}
function isRuleForRoom(roomId, rule) {
if (rule.conditions.length !== 1) {
return false;
}
const cond = rule.conditions[0];
if (
cond.kind == 'event_match' &&
cond.key == 'room_id' &&
cond.pattern == roomId
) {
return true;
}
return false;
}
function isMuteRule(rule) {
return (
rule.actions.length == 1 &&
rule.actions[0] == 'dont_notify'
);
}

View file

@ -34,8 +34,10 @@ class ScalarAuthClient {
defer.reject(err); defer.reject(err);
} else if (response.statusCode / 100 !== 2) { } else if (response.statusCode / 100 !== 2) {
defer.reject({statusCode: response.statusCode}); defer.reject({statusCode: response.statusCode});
} else if (!body || !body.scalar_token) {
defer.reject(new Error("Missing scalar_token in response"));
} else { } else {
defer.resolve(body.access_token); defer.resolve(body.scalar_token);
} }
}); });

View file

@ -1,4 +1,7 @@
"use strict"; "use strict";
import Matrix from "matrix-js-sdk";
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
var SignupStages = require("./SignupStages"); var SignupStages = require("./SignupStages");
var dis = require("./dispatcher"); var dis = require("./dispatcher");
@ -11,9 +14,10 @@ const EMAIL_STAGE_TYPE = "m.login.email.identity";
* storage of HS/IS URLs. * storage of HS/IS URLs.
*/ */
class Signup { class Signup {
constructor(hsUrl, isUrl) { constructor(hsUrl, isUrl, opts) {
this._hsUrl = hsUrl; this._hsUrl = hsUrl;
this._isUrl = isUrl; this._isUrl = isUrl;
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
} }
getHomeserverUrl() { getHomeserverUrl() {
@ -31,14 +35,25 @@ class Signup {
setIdentityServerUrl(isUrl) { setIdentityServerUrl(isUrl) {
this._isUrl = isUrl; this._isUrl = isUrl;
} }
/**
* Get a temporary MatrixClient, which can be used for login or register
* requests.
*/
_createTemporaryClient() {
return Matrix.createClient({
baseUrl: this._hsUrl,
idBaseUrl: this._isUrl,
});
}
} }
/** /**
* Registration logic class * Registration logic class
*/ */
class Register extends Signup { class Register extends Signup {
constructor(hsUrl, isUrl) { constructor(hsUrl, isUrl, opts) {
super(hsUrl, isUrl); super(hsUrl, isUrl, opts);
this.setStep("START"); this.setStep("START");
this.data = null; // from the server this.data = null; // from the server
// random other stuff (e.g. query params, NOT params from the server) // random other stuff (e.g. query params, NOT params from the server)
@ -106,19 +121,11 @@ class Register extends Signup {
this.email = email; this.email = email;
this.username = username; this.username = username;
this.password = password; this.password = password;
const client = this._createTemporaryClient();
// feels a bit wrong to be clobbering the global client for something we return this._tryRegister(client);
// don't even know if it'll work, but we'll leave this here for now to
// not complicate matters further. It would be nicer to isolate this
// logic entirely from the rest of the app though.
MatrixClientPeg.replaceUsingUrls(
this._hsUrl,
this._isUrl
);
return this._tryRegister();
} }
_tryRegister(authDict, poll_for_success) { _tryRegister(client, authDict, poll_for_success) {
var self = this; var self = this;
var bindEmail; var bindEmail;
@ -129,7 +136,8 @@ class Register extends Signup {
bindEmail = true; bindEmail = true;
} }
return MatrixClientPeg.get().register( // TODO need to figure out how to send the device display name to /register.
return client.register(
this.username, this.password, this.params.sessionId, authDict, bindEmail, this.username, this.password, this.params.sessionId, authDict, bindEmail,
this.guestAccessToken this.guestAccessToken
).then(function(result) { ).then(function(result) {
@ -152,7 +160,7 @@ class Register extends Signup {
console.log("Active flow => %s", JSON.stringify(flow)); console.log("Active flow => %s", JSON.stringify(flow));
var flowStage = self.firstUncompletedStage(flow); var flowStage = self.firstUncompletedStage(flow);
if (flowStage != self.activeStage) { if (flowStage != self.activeStage) {
return self.startStage(flowStage).catch(function(err) { return self._startStage(client, flowStage).catch(function(err) {
self.setStep('START'); self.setStep('START');
throw err; throw err;
}); });
@ -161,7 +169,7 @@ class Register extends Signup {
} }
if (poll_for_success) { if (poll_for_success) {
return q.delay(5000).then(function() { return q.delay(5000).then(function() {
return self._tryRegister(authDict, poll_for_success); return self._tryRegister(client, authDict, poll_for_success);
}); });
} else { } else {
throw new Error("Authorisation failed!"); throw new Error("Authorisation failed!");
@ -201,7 +209,7 @@ class Register extends Signup {
return completed.indexOf(stageType) !== -1; return completed.indexOf(stageType) !== -1;
} }
startStage(stageName) { _startStage(client, stageName) {
var self = this; var self = this;
this.setStep(`STEP_${stageName}`); this.setStep(`STEP_${stageName}`);
var StageClass = SignupStages[stageName]; var StageClass = SignupStages[stageName];
@ -210,12 +218,12 @@ class Register extends Signup {
throw new Error("Unknown stage: " + stageName); throw new Error("Unknown stage: " + stageName);
} }
var stage = new StageClass(MatrixClientPeg.get(), this); var stage = new StageClass(client, this);
this.activeStage = stage; this.activeStage = stage;
return stage.complete().then(function(request) { return stage.complete().then(function(request) {
if (request.auth) { if (request.auth) {
console.log("Stage %s is returning an auth dict", stageName); console.log("Stage %s is returning an auth dict", stageName);
return self._tryRegister(request.auth, request.poll_for_success); return self._tryRegister(client, request.auth, request.poll_for_success);
} }
else { else {
// never resolve the promise chain. This is for things like email auth // never resolve the promise chain. This is for things like email auth
@ -263,14 +271,6 @@ class Register extends Signup {
} }
recheckState() { recheckState() {
// feels a bit wrong to be clobbering the global client for something we
// don't even know if it'll work, but we'll leave this here for now to
// not complicate matters further. It would be nicer to isolate this
// logic entirely from the rest of the app though.
MatrixClientPeg.replaceUsingUrls(
this._hsUrl,
this._isUrl
);
// We've been given a bunch of data from a previous register step, // We've been given a bunch of data from a previous register step,
// this only happens for email auth currently. It's kinda ming we need // this only happens for email auth currently. It's kinda ming we need
// to know this though. A better solution would be to ask the stages if // to know this though. A better solution would be to ask the stages if
@ -281,7 +281,8 @@ class Register extends Signup {
); );
if (this.params.hasEmailInfo) { if (this.params.hasEmailInfo) {
this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE); const client = this._createTemporaryClient();
this.registrationPromise = this._startStage(client, EMAIL_STAGE_TYPE);
} }
return this.registrationPromise; return this.registrationPromise;
} }
@ -296,8 +297,8 @@ class Register extends Signup {
class Login extends Signup { class Login extends Signup {
constructor(hsUrl, isUrl, fallbackHsUrl) { constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
super(hsUrl, isUrl); super(hsUrl, isUrl, opts);
this._fallbackHsUrl = fallbackHsUrl; this._fallbackHsUrl = fallbackHsUrl;
this._currentFlowIndex = 0; this._currentFlowIndex = 0;
this._flows = []; this._flows = [];
@ -305,15 +306,8 @@ class Login extends Signup {
getFlows() { getFlows() {
var self = this; var self = this;
// feels a bit wrong to be clobbering the global client for something we var client = this._createTemporaryClient();
// don't even know if it'll work, but we'll leave this here for now to return client.loginFlows().then(function(result) {
// not complicate matters further. It would be nicer to isolate this
// logic entirely from the rest of the app though.
MatrixClientPeg.replaceUsingUrls(
this._hsUrl,
this._isUrl
);
return MatrixClientPeg.get().loginFlows().then(function(result) {
self._flows = result.flows; self._flows = result.flows;
self._currentFlowIndex = 0; self._currentFlowIndex = 0;
// technically the UI should display options for all flows for the // technically the UI should display options for all flows for the
@ -334,10 +328,15 @@ class Login extends Signup {
} }
loginAsGuest() { loginAsGuest() {
MatrixClientPeg.replaceUsingUrls(this._hsUrl, this._isUrl); var client = this._createTemporaryClient();
return MatrixClientPeg.get().registerGuest().then((creds) => { return client.registerGuest({
body: {
initial_device_display_name: this._defaultDeviceDisplayName,
},
}).then((creds) => {
return { return {
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id,
accessToken: creds.access_token, accessToken: creds.access_token,
homeserverUrl: this._hsUrl, homeserverUrl: this._hsUrl,
identityServerUrl: this._isUrl, identityServerUrl: this._isUrl,
@ -357,7 +356,8 @@ class Login extends Signup {
var self = this; var self = this;
var isEmail = username.indexOf("@") > 0; var isEmail = username.indexOf("@") > 0;
var loginParams = { var loginParams = {
password: pass password: pass,
initial_device_display_name: this._defaultDeviceDisplayName,
}; };
if (isEmail) { if (isEmail) {
loginParams.medium = 'email'; loginParams.medium = 'email';
@ -366,11 +366,13 @@ class Login extends Signup {
loginParams.user = username; loginParams.user = username;
} }
return MatrixClientPeg.get().login('m.login.password', loginParams).then(function(data) { var client = this._createTemporaryClient();
return client.login('m.login.password', loginParams).then(function(data) {
return q({ return q({
homeserverUrl: self._hsUrl, homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl, identityServerUrl: self._isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token accessToken: data.access_token
}); });
}, function(error) { }, function(error) {
@ -384,25 +386,20 @@ class Login extends Signup {
'Incorrect username and/or password.' 'Incorrect username and/or password.'
); );
if (self._fallbackHsUrl) { if (self._fallbackHsUrl) {
// as per elsewhere, it would be much nicer to not replace the global var fbClient = Matrix.createClient({
// client just to try an alternate HS baseUrl: self._fallbackHsUrl,
MatrixClientPeg.replaceUsingUrls( idBaseUrl: this._isUrl,
self._fallbackHsUrl, });
self._isUrl
); return fbClient.login('m.login.password', loginParams).then(function(data) {
return MatrixClientPeg.get().login('m.login.password', loginParams).then(function(data) {
return q({ return q({
homeserverUrl: self._fallbackHsUrl, homeserverUrl: self._fallbackHsUrl,
identityServerUrl: self._isUrl, identityServerUrl: self._isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token accessToken: data.access_token
}); });
}, function(fallback_error) { }, function(fallback_error) {
// We also have to put the default back again if it fails...
MatrixClientPeg.replaceUsingUrls(
this._hsUrl,
this._isUrl
);
// throw the original error // throw the original error
throw error; throw error;
}); });

View file

@ -94,7 +94,7 @@ CommandEntry.fromCommands = function(commandArray) {
class MemberEntry extends Entry { class MemberEntry extends Entry {
constructor(member) { constructor(member) {
super(member.name || member.userId); super((member.name || member.userId).replace(' (IRC)', ''));
this.member = member; this.member = member;
this.kind = 'member'; this.kind = 'member';
} }

View file

@ -58,6 +58,10 @@ module.exports = React.createClass({
// called when the session load completes // called when the session load completes
onLoadCompleted: React.PropTypes.func, onLoadCompleted: React.PropTypes.func,
// displayname, if any, to set on the device when logging
// in/registering.
defaultDeviceDisplayName: React.PropTypes.string,
}, },
PageTypes: { PageTypes: {
@ -185,6 +189,7 @@ module.exports = React.createClass({
enableGuest: this.props.enableGuest, enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(), guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(), guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}).done(()=>{ }).done(()=>{
// stuff this through the dispatcher so that it happens // stuff this through the dispatcher so that it happens
// after the on_logged_in action. // after the on_logged_in action.
@ -193,7 +198,7 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this._stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
@ -601,16 +606,6 @@ module.exports = React.createClass({
}); });
}, },
// stop all the background processes related to the current client
_stopMatrixClient: function() {
Notifier.stop();
UserActivity.stop();
Presence.stop();
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.unset();
},
onKeyDown: function(ev) { onKeyDown: function(ev) {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -935,10 +930,8 @@ module.exports = React.createClass({
var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
// work out the HS URL prompts we should show for // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
"; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
if (this.state.loading) { if (this.state.loading) {
var Spinner = sdk.getComponent('elements.Spinner'); var Spinner = sdk.getComponent('elements.Spinner');
@ -1051,6 +1044,7 @@ module.exports = React.createClass({
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl} registrationUrl={this.props.registrationUrl}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onLoggedIn={this.onRegistered} onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
@ -1077,6 +1071,7 @@ module.exports = React.createClass({
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()} fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest} enableGuest={this.props.enableGuest}
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}

View file

@ -44,6 +44,8 @@ module.exports = React.createClass({
// different home server without confusing users. // different home server without confusing users.
fallbackHsUrl: React.PropTypes.string, fallbackHsUrl: React.PropTypes.string,
defaultDeviceDisplayName: React.PropTypes.string,
// login shouldn't know or care how registration is done. // login shouldn't know or care how registration is done.
onRegisterClick: React.PropTypes.func.isRequired, onRegisterClick: React.PropTypes.func.isRequired,
@ -136,7 +138,9 @@ module.exports = React.createClass({
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl); var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
this._loginLogic = loginLogic; this._loginLogic = loginLogic;
loginLogic.getFlows().then(function(flows) { loginLogic.getFlows().then(function(flows) {

View file

@ -45,6 +45,9 @@ module.exports = React.createClass({
email: React.PropTypes.string, email: React.PropTypes.string,
username: React.PropTypes.string, username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string, guestAccessToken: React.PropTypes.string,
defaultDeviceDisplayName: React.PropTypes.string,
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired, onLoginClick: React.PropTypes.func.isRequired,
onCancelClick: React.PropTypes.func onCancelClick: React.PropTypes.func
@ -71,7 +74,9 @@ module.exports = React.createClass({
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI // attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register( this.registerLogic = new Signup.Register(
this.props.customHsUrl, this.props.customIsUrl this.props.customHsUrl, this.props.customIsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}
); );
this.registerLogic.setClientSecret(this.props.clientSecret); this.registerLogic.setClientSecret(this.props.clientSecret);
this.registerLogic.setSessionId(this.props.sessionId); this.registerLogic.setSessionId(this.props.sessionId);
@ -154,6 +159,7 @@ module.exports = React.createClass({
} }
self.props.onLoggedIn({ self.props.onLoggedIn({
userId: response.user_id, userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: self.registerLogic.getHomeserverUrl(), homeserverUrl: self.registerLogic.getHomeserverUrl(),
identityServerUrl: self.registerLogic.getIdentityServerUrl(), identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token

View file

@ -60,7 +60,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={cli.mxcUrlToHttp(content.url)} target="_blank"> <a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/> <TintableSvg src="img/download.svg" width="12" height="14"/>
Download {text} Download {text}
</a> </a>

View file

@ -134,7 +134,7 @@ module.exports = React.createClass({
onMouseLeave={this.onImageLeave} /> onMouseLeave={this.onImageLeave} />
</a> </a>
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank"> <a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/> <TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a> </a>

View file

@ -123,7 +123,7 @@ module.exports = React.createClass({
<div className="mx_LinkPreviewWidget" > <div className="mx_LinkPreviewWidget" >
{ img } { img }
<div className="mx_LinkPreviewWidget_caption"> <div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank">{ p["og:title"] }</a></div> <div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank" rel="noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div> <div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref="description"> <div className="mx_LinkPreviewWidget_description" ref="description">
{ p["og:description"] } { p["og:description"] }

View file

@ -67,6 +67,11 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._cancelDeviceList = null; this._cancelDeviceList = null;
// only display the devices list if our client supports E2E *and* the
// feature is enabled in the user settings
this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() &&
UserSettingsStore.isFeatureEnabled("e2e_encryption");
this.setState({ this.setState({
existingOneToOneRoomId: this.getExistingOneToOneRoomId() existingOneToOneRoomId: this.getExistingOneToOneRoomId()
}); });
@ -147,6 +152,10 @@ module.exports = React.createClass({
}, },
onDeviceVerificationChanged: function(userId, device) { onDeviceVerificationChanged: function(userId, device) {
if (!this._enableDevices) {
return;
}
if (userId == this.props.member.userId) { if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of // no need to re-download the whole thing; just update our copy of
// the list. // the list.
@ -170,6 +179,10 @@ module.exports = React.createClass({
}, },
_downloadDeviceList: function(member) { _downloadDeviceList: function(member) {
if (!this._enableDevices) {
return;
}
var cancelled = false; var cancelled = false;
this._cancelDeviceList = function() { cancelled = true; } this._cancelDeviceList = function() { cancelled = true; }
@ -532,7 +545,7 @@ module.exports = React.createClass({
}, },
_renderDevices: function() { _renderDevices: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { if (!this._enableDevices) {
return null; return null;
} }

View file

@ -180,7 +180,7 @@ module.exports = React.createClass({
}, },
_doInvite(address) { _doInvite(address) {
Invite.inviteToRoom(self.props.roomId, address).catch((err) => { Invite.inviteToRoom(this.props.roomId, address).catch((err) => {
if (err !== null) { if (err !== null) {
console.error("Failed to invite: %s", JSON.stringify(err)); console.error("Failed to invite: %s", JSON.stringify(err));
if (err.errcode == 'M_FORBIDDEN') { if (err.errcode == 'M_FORBIDDEN') {
@ -196,7 +196,7 @@ module.exports = React.createClass({
} }
} }
}).finally(() => { }).finally(() => {
self.setState({ this.setState({
inviting: false inviting: false
}); });
// XXX: hacky focus on the invite box // XXX: hacky focus on the invite box
@ -207,7 +207,7 @@ module.exports = React.createClass({
} }
}, 0); }, 0);
}).done(); }).done();
self.setState({ this.setState({
inviting: true inviting: true
}); });
}, },
@ -283,7 +283,7 @@ module.exports = React.createClass({
if (inputs.length == 1) { if (inputs.length == 1) {
// for a single address, we just send the invite // for a single address, we just send the invite
promise.done(() => { promise.done(() => {
this.doInvite(inputs[0]); this._doInvite(inputs[0]);
}); });
} else { } else {
// if there are several, display the confirmation/progress dialog // if there are several, display the confirmation/progress dialog

View file

@ -47,16 +47,6 @@ module.exports = React.createClass({
tags[tagName] = ['yep']; tags[tagName] = ['yep'];
}); });
var areNotifsMuted = false;
if (!MatrixClientPeg.get().isGuest()) {
var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return { return {
name: this._yankValueFromEvent("m.room.name", "name"), name: this._yankValueFromEvent("m.room.name", "name"),
topic: this._yankValueFromEvent("m.room.topic", "topic"), topic: this._yankValueFromEvent("m.room.topic", "topic"),
@ -66,7 +56,6 @@ module.exports = React.createClass({
power_levels_changed: false, power_levels_changed: false,
tags_changed: false, tags_changed: false,
tags: tags, tags: tags,
areNotifsMuted: areNotifsMuted,
// isRoomPublished is loaded async in componentWillMount so when the component // isRoomPublished is loaded async in componentWillMount so when the component
// inits, the saved value will always be undefined, however getInitialState() // inits, the saved value will always be undefined, however getInitialState()
// is also called from the saving code so we must return the correct value here // is also called from the saving code so we must return the correct value here
@ -188,12 +177,6 @@ module.exports = React.createClass({
} }
if (this.state.areNotifsMuted !== originalState.areNotifsMuted) {
promises.push(MatrixClientPeg.get().setRoomMutePushRule(
"global", roomId, this.state.areNotifsMuted
));
}
// power levels // power levels
var powerLevels = this._getPowerLevels(); var powerLevels = this._getPowerLevels();
if (powerLevels) { if (powerLevels) {
@ -647,12 +630,6 @@ module.exports = React.createClass({
{ tagsSection } { tagsSection }
<div className="mx_RoomSettings_toggles"> <div className="mx_RoomSettings_toggles">
<label>
<input type="checkbox" disabled={ cli.isGuest() }
onChange={this._onToggle.bind(this, "areNotifsMuted", true, false)}
defaultChecked={this.state.areNotifsMuted}/>
'Mention only' notifications for this room
</label>
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
<h3>Who can access this room?</h3> <h3>Who can access this room?</h3>
{ inviteGuestWarning } { inviteGuestWarning }

View file

@ -22,6 +22,7 @@ var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu'); var ContextualMenu = require('../../structures/ContextualMenu');
var RoomNotifs = require('../../../RoomNotifs');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -43,43 +44,41 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
var areNotifsMuted = false;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return({ return({
hover : false, hover : false,
badgeHover : false, badgeHover : false,
notificationTagMenu: false, notificationTagMenu: false,
roomTagMenu: false, roomTagMenu: false,
areNotifsMuted: areNotifsMuted, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
}); });
}, },
onAction: function(payload) { _shouldShowNotifBadge: function() {
switch (payload.action) { const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
case 'notification_change': return showBadgeInStates.indexOf(this.state.notifState) > -1;
// Is the notification about this room? },
if (payload.roomId === this.props.room.roomId) {
this.setState( { areNotifsMuted : payload.areNotifsMuted }); _shouldShowMentionBadge: function() {
} return this.state.notifState != RoomNotifs.MUTE;
break; },
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
} }
}, },
componentDidMount: function() { componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("accountData", this.onAccountData);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
dis.unregister(this.dispatcherRef); var cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
}, },
onClick: function() { onClick: function() {
@ -179,15 +178,19 @@ module.exports = React.createClass({
var notificationCount = this.props.room.getUnreadNotificationCount(); var notificationCount = this.props.room.getUnreadNotificationCount();
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({ var classes = classNames({
'mx_RoomTile': true, 'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0 && !this.state.areNotifsMuted, 'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu, 'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu,
'mx_RoomTile_noBadges': !(this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted)) 'mx_RoomTile_noBadges': !badges,
}); });
var avatarClasses = classNames({ var avatarClasses = classNames({
@ -214,7 +217,7 @@ module.exports = React.createClass({
if (this.state.badgeHover || this.state.notificationTagMenu) { if (this.state.badgeHover || this.state.notificationTagMenu) {
badgeContent = "\u00B7\u00B7\u00B7"; badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted)) { } else if (badges) {
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!'; badgeContent = notificationCount ? limitedCount : '!';
} else { } else {
@ -230,7 +233,7 @@ module.exports = React.createClass({
var nameClasses = classNames({ var nameClasses = classNames({
'mx_RoomTile_name': true, 'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite, 'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted) || this.state.badgeHover || this.state.notificationTagMenu, 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu,
}); });
if (this.props.selected) { if (this.props.selected) {

View file

@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component {
(error) => { (error) => {
if (this._unmounted) { return; } if (this._unmounted) { return; }
var errtxt; var errtxt;
if (err.httpStatus == 404) { if (error.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API. // 404 probably means the HS doesn't yet support the API.
errtxt = "Your home server does not support device management."; errtxt = "Your home server does not support device management.";
} else { } else {
@ -127,6 +127,7 @@ export default class DevicesPanel extends React.Component {
return ( return (
<div className={classes}> <div className={classes}>
<div className="mx_DevicesPanel_header"> <div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_deviceId">ID</div>
<div className="mx_DevicesPanel_deviceName">Name</div> <div className="mx_DevicesPanel_deviceName">Name</div>
<div className="mx_DevicesPanel_deviceLastSeen">Last seen</div> <div className="mx_DevicesPanel_deviceLastSeen">Last seen</div>
<div className="mx_DevicesPanel_deviceButtons"></div> <div className="mx_DevicesPanel_deviceButtons"></div>

View file

@ -109,6 +109,9 @@ export default class DevicesPanelEntry extends React.Component {
return ( return (
<div className="mx_DevicesPanel_device"> <div className="mx_DevicesPanel_device">
<div className="mx_DevicesPanel_deviceId">
{device.device_id}
</div>
<div className="mx_DevicesPanel_deviceName"> <div className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name} <EditableTextContainer initialValue={device.display_name}
onSubmit={this._onDisplayNameChanged} onSubmit={this._onDisplayNameChanged}

View file

@ -137,6 +137,10 @@ matrixLinkify.options = {
} }
}, },
linkAttributes: {
rel: 'noopener',
},
target: function(href, type) { target: function(href, type) {
if (type === 'url') { if (type === 'url') {
if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) { if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) {

View file

@ -50,8 +50,7 @@ module.exports.stubClient = function() {
// //
// 'sandbox.restore()' doesn't work correctly on inherited methods, // 'sandbox.restore()' doesn't work correctly on inherited methods,
// so we do this for each method // so we do this for each method
var methods = ['get', 'unset', 'replaceUsingUrls', var methods = ['get', 'unset', 'replaceUsingCreds'];
'replaceUsingCreds'];
for (var i = 0; i < methods.length; i++) { for (var i = 0; i < methods.length; i++) {
sandbox.stub(peg, methods[i]); sandbox.stub(peg, methods[i]);
} }
@ -184,4 +183,3 @@ module.exports.mkStubRoom = function() {
}, },
}; };
}; };