Merge branch 'develop' into matthew/whitelist-uri-schemes
commit
ec2a5cce74
|
@ -0,0 +1,23 @@
|
|||
# Copyright 2017 Aviral Dasgupta
|
||||
#
|
||||
# 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.
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
246
CHANGELOG.md
246
CHANGELOG.md
|
@ -1,3 +1,249 @@
|
|||
Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7)
|
||||
|
||||
* No changes
|
||||
|
||||
Changes in [0.8.7-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.4) (2017-04-11)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.3...v0.8.7-rc.4)
|
||||
|
||||
* Fix people section vanishing on 'clear cache'
|
||||
[\#799](https://github.com/matrix-org/matrix-react-sdk/pull/799)
|
||||
* Make the clear cache button work on desktop
|
||||
[\#798](https://github.com/matrix-org/matrix-react-sdk/pull/798)
|
||||
|
||||
Changes in [0.8.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.3) (2017-04-10)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.2...v0.8.7-rc.3)
|
||||
|
||||
* Use matrix-js-sdk v0.7.6-rc.2
|
||||
|
||||
|
||||
Changes in [0.8.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.2) (2017-04-10)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.1...v0.8.7-rc.2)
|
||||
|
||||
* fix the warning shown to users about needing to export e2e keys
|
||||
[\#797](https://github.com/matrix-org/matrix-react-sdk/pull/797)
|
||||
|
||||
Changes in [0.8.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.1) (2017-04-07)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6...v0.8.7-rc.1)
|
||||
|
||||
* Add support for using indexeddb in a webworker
|
||||
[\#792](https://github.com/matrix-org/matrix-react-sdk/pull/792)
|
||||
* Fix infinite pagination/glitches with pagination
|
||||
[\#795](https://github.com/matrix-org/matrix-react-sdk/pull/795)
|
||||
* Fix issue where teamTokenMap was ignored for guests
|
||||
[\#793](https://github.com/matrix-org/matrix-react-sdk/pull/793)
|
||||
* Click emote sender -> insert display name into composer
|
||||
[\#791](https://github.com/matrix-org/matrix-react-sdk/pull/791)
|
||||
* Fix scroll token selection logic
|
||||
[\#785](https://github.com/matrix-org/matrix-react-sdk/pull/785)
|
||||
* Replace sdkReady with firstSyncPromise, add mx_last_room_id
|
||||
[\#790](https://github.com/matrix-org/matrix-react-sdk/pull/790)
|
||||
* Change "Unread messages." to "Jump to first unread message."
|
||||
[\#789](https://github.com/matrix-org/matrix-react-sdk/pull/789)
|
||||
* Update for new IndexedDBStore interface
|
||||
[\#786](https://github.com/matrix-org/matrix-react-sdk/pull/786)
|
||||
* Add <ol start="..."> to allowed attributes list
|
||||
[\#787](https://github.com/matrix-org/matrix-react-sdk/pull/787)
|
||||
* Fix the onFinished for timeline pos dialog
|
||||
[\#784](https://github.com/matrix-org/matrix-react-sdk/pull/784)
|
||||
* Only join a room when enter is hit if the join button is shown
|
||||
[\#776](https://github.com/matrix-org/matrix-react-sdk/pull/776)
|
||||
* Remove non-functional session load error
|
||||
[\#783](https://github.com/matrix-org/matrix-react-sdk/pull/783)
|
||||
* Use Login & Register via component interface
|
||||
[\#782](https://github.com/matrix-org/matrix-react-sdk/pull/782)
|
||||
* Attempt to fix the flakyness seen with tests
|
||||
[\#781](https://github.com/matrix-org/matrix-react-sdk/pull/781)
|
||||
* Remove React warning
|
||||
[\#780](https://github.com/matrix-org/matrix-react-sdk/pull/780)
|
||||
* Only clear the local notification count if needed
|
||||
[\#779](https://github.com/matrix-org/matrix-react-sdk/pull/779)
|
||||
* Don't re-notify about messages on browser refresh
|
||||
[\#777](https://github.com/matrix-org/matrix-react-sdk/pull/777)
|
||||
* Improve zeroing of RoomList notification badges
|
||||
[\#775](https://github.com/matrix-org/matrix-react-sdk/pull/775)
|
||||
* Fix VOIP bar hidden on first render of RoomStatusBar
|
||||
[\#774](https://github.com/matrix-org/matrix-react-sdk/pull/774)
|
||||
* Correct confirm prompt for disinvite
|
||||
[\#772](https://github.com/matrix-org/matrix-react-sdk/pull/772)
|
||||
* Add state loggingIn to MatrixChat to fix flashing login
|
||||
[\#773](https://github.com/matrix-org/matrix-react-sdk/pull/773)
|
||||
* Fix bug where you can't invite a valid address
|
||||
[\#771](https://github.com/matrix-org/matrix-react-sdk/pull/771)
|
||||
* Fix people section DropTarget and refactor Rooms
|
||||
[\#761](https://github.com/matrix-org/matrix-react-sdk/pull/761)
|
||||
* Read Receipt offset
|
||||
[\#770](https://github.com/matrix-org/matrix-react-sdk/pull/770)
|
||||
* Support adding phone numbers in UserSettings
|
||||
[\#756](https://github.com/matrix-org/matrix-react-sdk/pull/756)
|
||||
* Prevent crash on login of no guest session
|
||||
[\#769](https://github.com/matrix-org/matrix-react-sdk/pull/769)
|
||||
* Add canResetTimeline callback and thread it through to TimelinePanel
|
||||
[\#768](https://github.com/matrix-org/matrix-react-sdk/pull/768)
|
||||
* Show spinner whilst processing recaptcha response
|
||||
[\#767](https://github.com/matrix-org/matrix-react-sdk/pull/767)
|
||||
* Login / registration with phone number, mark 2
|
||||
[\#750](https://github.com/matrix-org/matrix-react-sdk/pull/750)
|
||||
* Display threepids slightly prettier
|
||||
[\#758](https://github.com/matrix-org/matrix-react-sdk/pull/758)
|
||||
* Fix extraneous leading space in sent emotes
|
||||
[\#764](https://github.com/matrix-org/matrix-react-sdk/pull/764)
|
||||
* Add ConfirmRedactDialog component
|
||||
[\#763](https://github.com/matrix-org/matrix-react-sdk/pull/763)
|
||||
* Fix password UI auth test
|
||||
[\#760](https://github.com/matrix-org/matrix-react-sdk/pull/760)
|
||||
* Display timestamps and profiles for redacted events
|
||||
[\#759](https://github.com/matrix-org/matrix-react-sdk/pull/759)
|
||||
* Fix UDD for voip in e2e rooms
|
||||
[\#757](https://github.com/matrix-org/matrix-react-sdk/pull/757)
|
||||
* Add "Export E2E keys" option to logout dialog
|
||||
[\#755](https://github.com/matrix-org/matrix-react-sdk/pull/755)
|
||||
* Fix People section a bit
|
||||
[\#754](https://github.com/matrix-org/matrix-react-sdk/pull/754)
|
||||
* Do routing to /register _onLoadCompleted
|
||||
[\#753](https://github.com/matrix-org/matrix-react-sdk/pull/753)
|
||||
* Double UNPAGINATION_PADDING again
|
||||
[\#747](https://github.com/matrix-org/matrix-react-sdk/pull/747)
|
||||
* Add null check to start_login
|
||||
[\#751](https://github.com/matrix-org/matrix-react-sdk/pull/751)
|
||||
* Merge the two RoomTile context menus into one
|
||||
[\#746](https://github.com/matrix-org/matrix-react-sdk/pull/746)
|
||||
* Fix import for Lifecycle
|
||||
[\#748](https://github.com/matrix-org/matrix-react-sdk/pull/748)
|
||||
* Make UDD appear when UDE on uploading a file
|
||||
[\#745](https://github.com/matrix-org/matrix-react-sdk/pull/745)
|
||||
* Decide on which screen to show after login in one place
|
||||
[\#743](https://github.com/matrix-org/matrix-react-sdk/pull/743)
|
||||
* Add onClick to permalinks to route within Riot
|
||||
[\#744](https://github.com/matrix-org/matrix-react-sdk/pull/744)
|
||||
* Add support for pasting files into the text box
|
||||
[\#605](https://github.com/matrix-org/matrix-react-sdk/pull/605)
|
||||
* Show message redactions as black event tiles
|
||||
[\#739](https://github.com/matrix-org/matrix-react-sdk/pull/739)
|
||||
* Allow user to choose from existing DMs on new chat
|
||||
[\#736](https://github.com/matrix-org/matrix-react-sdk/pull/736)
|
||||
* Fix the team server registration
|
||||
[\#741](https://github.com/matrix-org/matrix-react-sdk/pull/741)
|
||||
* Clarify "No devices" message
|
||||
[\#740](https://github.com/matrix-org/matrix-react-sdk/pull/740)
|
||||
* Change timestamp permalinks to matrix.to
|
||||
[\#735](https://github.com/matrix-org/matrix-react-sdk/pull/735)
|
||||
* Fix resend bar and "send anyway" in UDD
|
||||
[\#734](https://github.com/matrix-org/matrix-react-sdk/pull/734)
|
||||
* Make COLOR_REGEX stricter
|
||||
[\#737](https://github.com/matrix-org/matrix-react-sdk/pull/737)
|
||||
* Port registration over to use InteractiveAuth
|
||||
[\#729](https://github.com/matrix-org/matrix-react-sdk/pull/729)
|
||||
* Test to see how fuse feels
|
||||
[\#732](https://github.com/matrix-org/matrix-react-sdk/pull/732)
|
||||
* Submit a new display name on blur of input field
|
||||
[\#733](https://github.com/matrix-org/matrix-react-sdk/pull/733)
|
||||
* Allow [bf]g colors for <font> style attrib
|
||||
[\#610](https://github.com/matrix-org/matrix-react-sdk/pull/610)
|
||||
* MELS: either expanded or summary, not both
|
||||
[\#683](https://github.com/matrix-org/matrix-react-sdk/pull/683)
|
||||
* Autoplay videos and GIFs if enabled by the user.
|
||||
[\#730](https://github.com/matrix-org/matrix-react-sdk/pull/730)
|
||||
* Warn users about using e2e for the first time
|
||||
[\#731](https://github.com/matrix-org/matrix-react-sdk/pull/731)
|
||||
* Show UDDialog on UDE during VoIP calls
|
||||
[\#721](https://github.com/matrix-org/matrix-react-sdk/pull/721)
|
||||
* Notify MatrixChat of teamToken after login
|
||||
[\#726](https://github.com/matrix-org/matrix-react-sdk/pull/726)
|
||||
* Fix a couple of issues with RRs
|
||||
[\#727](https://github.com/matrix-org/matrix-react-sdk/pull/727)
|
||||
* Do not push a dummy element with a scroll token for invisible events
|
||||
[\#718](https://github.com/matrix-org/matrix-react-sdk/pull/718)
|
||||
* MELS: check scroll on load + use mels-1,-2,... key
|
||||
[\#715](https://github.com/matrix-org/matrix-react-sdk/pull/715)
|
||||
* Fix message composer placeholders
|
||||
[\#723](https://github.com/matrix-org/matrix-react-sdk/pull/723)
|
||||
* Clarify non-e2e vs. e2e /w composers placeholder
|
||||
[\#720](https://github.com/matrix-org/matrix-react-sdk/pull/720)
|
||||
* Fix status bar expanded on tab-complete
|
||||
[\#722](https://github.com/matrix-org/matrix-react-sdk/pull/722)
|
||||
* add .editorconfig
|
||||
[\#713](https://github.com/matrix-org/matrix-react-sdk/pull/713)
|
||||
* Change the name of the database
|
||||
[\#719](https://github.com/matrix-org/matrix-react-sdk/pull/719)
|
||||
* Allow setting the default HS from the query parameter
|
||||
[\#716](https://github.com/matrix-org/matrix-react-sdk/pull/716)
|
||||
* first cut of improving UX for deleting devices.
|
||||
[\#717](https://github.com/matrix-org/matrix-react-sdk/pull/717)
|
||||
* Fix block quotes all being on a single line
|
||||
[\#711](https://github.com/matrix-org/matrix-react-sdk/pull/711)
|
||||
* Support reasons for kick / ban
|
||||
[\#710](https://github.com/matrix-org/matrix-react-sdk/pull/710)
|
||||
* Show when you've been kicked or banned
|
||||
[\#709](https://github.com/matrix-org/matrix-react-sdk/pull/709)
|
||||
* Add a 'Clear Cache' button
|
||||
[\#708](https://github.com/matrix-org/matrix-react-sdk/pull/708)
|
||||
* Update the room view on room name change
|
||||
[\#707](https://github.com/matrix-org/matrix-react-sdk/pull/707)
|
||||
* Add a button to un-ban users in RoomSettings
|
||||
[\#698](https://github.com/matrix-org/matrix-react-sdk/pull/698)
|
||||
* Use IndexedDBStore from the JS-SDK
|
||||
[\#687](https://github.com/matrix-org/matrix-react-sdk/pull/687)
|
||||
* Make UserSettings use the right teamToken
|
||||
[\#706](https://github.com/matrix-org/matrix-react-sdk/pull/706)
|
||||
* If the home page is somehow accessed, goto directory
|
||||
[\#705](https://github.com/matrix-org/matrix-react-sdk/pull/705)
|
||||
* Display avatar initials in typing notifications
|
||||
[\#699](https://github.com/matrix-org/matrix-react-sdk/pull/699)
|
||||
* fix eslint's no-invalid-this rule for class properties
|
||||
[\#703](https://github.com/matrix-org/matrix-react-sdk/pull/703)
|
||||
* If a referrer hasn't been specified, use empty string
|
||||
[\#701](https://github.com/matrix-org/matrix-react-sdk/pull/701)
|
||||
* Don't force-logout the user if reading localstorage fails
|
||||
[\#700](https://github.com/matrix-org/matrix-react-sdk/pull/700)
|
||||
* Convert some missed buttons to AccessibleButton
|
||||
[\#697](https://github.com/matrix-org/matrix-react-sdk/pull/697)
|
||||
* Make ban either ban or unban
|
||||
[\#696](https://github.com/matrix-org/matrix-react-sdk/pull/696)
|
||||
* Add confirmation dialog to kick/ban buttons
|
||||
[\#694](https://github.com/matrix-org/matrix-react-sdk/pull/694)
|
||||
* Fix typo with Scalar popup
|
||||
[\#695](https://github.com/matrix-org/matrix-react-sdk/pull/695)
|
||||
* Treat the literal team token string "undefined" as undefined
|
||||
[\#693](https://github.com/matrix-org/matrix-react-sdk/pull/693)
|
||||
* Store retrieved sid in the signupInstance of EmailIdentityStage
|
||||
[\#692](https://github.com/matrix-org/matrix-react-sdk/pull/692)
|
||||
* Split out InterActiveAuthDialog
|
||||
[\#691](https://github.com/matrix-org/matrix-react-sdk/pull/691)
|
||||
* View /home on registered /w team
|
||||
[\#689](https://github.com/matrix-org/matrix-react-sdk/pull/689)
|
||||
* Instead of sending userId, userEmail, send sid, client_secret
|
||||
[\#688](https://github.com/matrix-org/matrix-react-sdk/pull/688)
|
||||
* Enable branded URLs again by parsing the path client-side
|
||||
[\#686](https://github.com/matrix-org/matrix-react-sdk/pull/686)
|
||||
* Use new method of getting team icon
|
||||
[\#680](https://github.com/matrix-org/matrix-react-sdk/pull/680)
|
||||
* Persist query parameter team token across refreshes
|
||||
[\#685](https://github.com/matrix-org/matrix-react-sdk/pull/685)
|
||||
* Thread teamToken through to LeftPanel for "Home" button
|
||||
[\#684](https://github.com/matrix-org/matrix-react-sdk/pull/684)
|
||||
* Fix typing notif and status bar
|
||||
[\#682](https://github.com/matrix-org/matrix-react-sdk/pull/682)
|
||||
* Consider emails ending in matrix.org as a uni email
|
||||
[\#681](https://github.com/matrix-org/matrix-react-sdk/pull/681)
|
||||
* Set referrer qp in nextLink
|
||||
[\#679](https://github.com/matrix-org/matrix-react-sdk/pull/679)
|
||||
* Do not set team_token if not returned by RTS on login
|
||||
[\#678](https://github.com/matrix-org/matrix-react-sdk/pull/678)
|
||||
* Get team_token from the RTS on login
|
||||
[\#676](https://github.com/matrix-org/matrix-react-sdk/pull/676)
|
||||
* Quick and dirty support for custom welcome pages
|
||||
[\#550](https://github.com/matrix-org/matrix-react-sdk/pull/550)
|
||||
* RTS Welcome Pages
|
||||
[\#666](https://github.com/matrix-org/matrix-react-sdk/pull/666)
|
||||
* Logging to try to track down riot-web#3148
|
||||
[\#677](https://github.com/matrix-org/matrix-react-sdk/pull/677)
|
||||
|
||||
Changes in [0.8.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6) (2017-02-04)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.3...v0.8.6)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.7",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -51,11 +52,36 @@ class AddThreepid {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to add a msisdn threepid. This will trigger a side-effect of
|
||||
* sending a test message to the provided phone number.
|
||||
* @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in
|
||||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
addMsisdn(phoneCountry, phoneNumber, bind) {
|
||||
this.bind = bind;
|
||||
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||
).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||
err.message = "This phone number is already in use";
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email link has been clicked by attempting to add the threepid
|
||||
* @return {Promise} Resolves if the password was reset. Rejects with an object
|
||||
* @return {Promise} Resolves if the email address was added. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||
* the request failed.
|
||||
*/
|
||||
checkEmailLinkClicked() {
|
||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
|
@ -73,6 +99,29 @@ class AddThreepid {
|
|||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a phone number verification code as entered by the user and validates
|
||||
* it with the ID server, then if successful, adds the phone number.
|
||||
* @return {Promise} Resolves if the phone number was added. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
haveMsisdnToken(token) {
|
||||
return MatrixClientPeg.get().submitMsisdnToken(
|
||||
this.sessionId, this.clientSecret, token,
|
||||
).then((result) => {
|
||||
if (result.errcode) {
|
||||
throw result;
|
||||
}
|
||||
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
return MatrixClientPeg.get().addThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: identityServerDomain
|
||||
}, this.bind);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AddThreepid;
|
||||
|
|
|
@ -82,4 +82,12 @@ export default class BasePlatform {
|
|||
screenCaptureErrorString() {
|
||||
return "Not implemented";
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the application, without neccessarily reloading
|
||||
* any application code
|
||||
*/
|
||||
reload() {
|
||||
throw new Error("reload not implemented!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,15 @@ function _setCallListeners(call) {
|
|||
call.hangup();
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
});
|
||||
call.on('send_event_error', function(err) {
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: MatrixClientPeg.get().getRoom(call.roomId),
|
||||
});
|
||||
}
|
||||
});
|
||||
call.on("hangup", function() {
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
});
|
||||
|
@ -301,9 +310,10 @@ function _onAction(payload) {
|
|||
placeCall(call);
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Conference call failed: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set up conference call",
|
||||
description: "Conference call failed: " + err,
|
||||
description: "Conference call failed.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ class ContentMessages {
|
|||
|
||||
sendContentToRoom(file, roomId, matrixClient) {
|
||||
const content = {
|
||||
body: file.name,
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
size: file.size,
|
||||
}
|
||||
|
@ -316,7 +316,7 @@ class ContentMessages {
|
|||
}
|
||||
|
||||
const upload = {
|
||||
fileName: file.name,
|
||||
fileName: file.name || 'Attachment',
|
||||
roomId: roomId,
|
||||
total: 0,
|
||||
loaded: 0,
|
||||
|
|
|
@ -28,6 +28,7 @@ emojione.imagePathSVG = 'emojione/svg/';
|
|||
emojione.imageType = 'svg';
|
||||
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||
* because we want to include emoji shortnames in title text
|
||||
|
@ -57,6 +58,22 @@ export function unicodeToImage(str) {
|
|||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one or more unicode characters (represented by unicode
|
||||
* character number), return an image node with the corresponding
|
||||
* emoji.
|
||||
*
|
||||
* @param alt {string} String to use for the image alt text
|
||||
* @param unicode {integer} One or more integers representing unicode characters
|
||||
* @returns A img node with the corresponding emoji
|
||||
*/
|
||||
export function charactersToImageNode(alt, ...unicode) {
|
||||
const fileName = unicode.map((u) => {
|
||||
return u.toString(16);
|
||||
}).join('-');
|
||||
return <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>;
|
||||
}
|
||||
|
||||
export function stripParagraphs(html: string): string {
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.innerHTML = html;
|
||||
|
@ -87,15 +104,17 @@ var sanitizeHtmlParams = {
|
|||
// deliberately no h1/h2 to stop people shouting.
|
||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||
],
|
||||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
font: ['color'], // custom to matrix
|
||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||
// We don't currently allow img itself by default, but this
|
||||
// would make sense if we did
|
||||
img: ['src'],
|
||||
ol: ['start'],
|
||||
},
|
||||
// Lots of these won't come up by default because we don't allow them
|
||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
|
@ -136,6 +155,38 @@ var sanitizeHtmlParams = {
|
|||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
return { tagName: tagName, attribs : attribs };
|
||||
},
|
||||
'*': function(tagName, attribs) {
|
||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||
// because attributes are stripped after transforming
|
||||
delete attribs.style;
|
||||
|
||||
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
|
||||
// equivalents
|
||||
const customCSSMapper = {
|
||||
'data-mx-color': 'color',
|
||||
'data-mx-bg-color': 'background-color',
|
||||
// $customAttributeKey: $cssAttributeKey
|
||||
};
|
||||
|
||||
let style = "";
|
||||
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
|
||||
const cssAttributeKey = customCSSMapper[customAttributeKey];
|
||||
const customAttributeValue = attribs[customAttributeKey];
|
||||
if (customAttributeValue &&
|
||||
typeof customAttributeValue === 'string' &&
|
||||
COLOR_REGEX.test(customAttributeValue)
|
||||
) {
|
||||
style += cssAttributeKey + ":" + customAttributeValue + ";";
|
||||
delete attribs[customAttributeKey];
|
||||
}
|
||||
});
|
||||
|
||||
if (style) {
|
||||
attribs.style = style;
|
||||
}
|
||||
|
||||
return { tagName: tagName, attribs: attribs };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@ import MultiInviter from './utils/MultiInviter';
|
|||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
// We allow localhost for mxids to avoid confusion
|
||||
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
|
||||
const mxidRegex = /^@\S+:\S+$/
|
||||
|
||||
export function getAddressType(inputText) {
|
||||
const isEmailAddress = emailRegex.test(inputText);
|
||||
|
|
|
@ -155,7 +155,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
|||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
console.log("Doing guest login on %s", hsUrl);
|
||||
|
||||
// TODO: we should probably de-duplicate this and Signup.Login.loginAsGuest.
|
||||
// TODO: we should probably de-duplicate this and Login.loginAsGuest.
|
||||
// Not really sure where the right home for it is.
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
|
@ -276,6 +276,14 @@ export function setLoggedIn(credentials) {
|
|||
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
||||
credentials.userId, credentials.guest,
|
||||
credentials.homeserverUrl);
|
||||
// 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
|
||||
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
|
||||
// later than MatrixChat might assume.
|
||||
dis.dispatch({action: 'on_logging_in'});
|
||||
|
||||
// Resolves by default
|
||||
let teamPromise = Promise.resolve(null);
|
||||
|
||||
// persist the session
|
||||
if (localStorage) {
|
||||
|
@ -300,25 +308,29 @@ export function setLoggedIn(credentials) {
|
|||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
|
||||
if (rtsClient) {
|
||||
rtsClient.login(credentials.userId).then((body) => {
|
||||
if (rtsClient && !credentials.guest) {
|
||||
teamPromise = rtsClient.login(credentials.userId).then((body) => {
|
||||
if (body.team_token) {
|
||||
localStorage.setItem("mx_team_token", body.team_token);
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error(
|
||||
"Failed to get team token on login, not persisting to localStorage",
|
||||
err
|
||||
);
|
||||
return body.team_token;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("No local storage available: can't persist session!");
|
||||
}
|
||||
|
||||
// stop any running clients before we create a new one with these new credentials
|
||||
stopMatrixClient();
|
||||
|
||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||
|
||||
dis.dispatch({action: 'on_logged_in'});
|
||||
teamPromise.then((teamToken) => {
|
||||
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||
}, (err) => {
|
||||
console.warn("Failed to get team token on login", err);
|
||||
dis.dispatch({action: 'on_logged_in', teamToken: null});
|
||||
});
|
||||
|
||||
startMatrixClient();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 Matrix from "matrix-js-sdk";
|
||||
|
||||
import q from 'q';
|
||||
import url from 'url';
|
||||
|
||||
export default class Login {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
this._isUrl = isUrl;
|
||||
this._fallbackHsUrl = fallbackHsUrl;
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
return this._hsUrl;
|
||||
}
|
||||
|
||||
getIdentityServerUrl() {
|
||||
return this._isUrl;
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(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,
|
||||
});
|
||||
}
|
||||
|
||||
getFlows() {
|
||||
var self = this;
|
||||
var client = this._createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
// technically the UI should display options for all flows for the
|
||||
// user to then choose one, so return all the flows here.
|
||||
return self._flows;
|
||||
});
|
||||
}
|
||||
|
||||
chooseFlow(flowIndex) {
|
||||
this._currentFlowIndex = flowIndex;
|
||||
}
|
||||
|
||||
getCurrentFlowStep() {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login so we can ignore it.
|
||||
var flowStep = this._flows[this._currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginAsGuest() {
|
||||
var client = this._createTemporaryClient();
|
||||
return client.registerGuest({
|
||||
body: {
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
},
|
||||
}).then((creds) => {
|
||||
return {
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: this._hsUrl,
|
||||
identityServerUrl: this._isUrl,
|
||||
guest: true
|
||||
};
|
||||
}, (error) => {
|
||||
if (error.httpStatus === 403) {
|
||||
error.friendlyText = "Guest access is disabled on this Home Server.";
|
||||
} else {
|
||||
error.friendlyText = "Failed to register as guest: " + error.data;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||
const self = this;
|
||||
|
||||
const isEmail = username.indexOf("@") > 0;
|
||||
|
||||
let identifier;
|
||||
let legacyParams; // parameters added to support old HSes
|
||||
if (phoneCountry && phoneNumber) {
|
||||
identifier = {
|
||||
type: 'm.id.phone',
|
||||
country: phoneCountry,
|
||||
number: phoneNumber,
|
||||
};
|
||||
// No legacy support for phone number login
|
||||
} else if (isEmail) {
|
||||
identifier = {
|
||||
type: 'm.id.thirdparty',
|
||||
medium: 'email',
|
||||
address: username,
|
||||
};
|
||||
legacyParams = {
|
||||
medium: 'email',
|
||||
address: username,
|
||||
};
|
||||
} else {
|
||||
identifier = {
|
||||
type: 'm.id.user',
|
||||
user: username,
|
||||
};
|
||||
legacyParams = {
|
||||
user: username,
|
||||
};
|
||||
}
|
||||
|
||||
const loginParams = {
|
||||
password: pass,
|
||||
identifier: identifier,
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
};
|
||||
Object.assign(loginParams, legacyParams);
|
||||
|
||||
const client = this._createTemporaryClient();
|
||||
return client.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
});
|
||||
}, function(error) {
|
||||
if (error.httpStatus == 400 && loginParams.medium) {
|
||||
error.friendlyText = (
|
||||
'This Home Server does not support login using email address.'
|
||||
);
|
||||
}
|
||||
else if (error.httpStatus === 403) {
|
||||
error.friendlyText = (
|
||||
'Incorrect username and/or password.'
|
||||
);
|
||||
if (self._fallbackHsUrl) {
|
||||
var fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
});
|
||||
}, function(fallback_error) {
|
||||
// throw the original error
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
error.friendlyText = (
|
||||
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
redirectToCas() {
|
||||
var client = this._createTemporaryClient();
|
||||
var parsedUrl = url.parse(window.location.href, true);
|
||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
||||
window.location.href = casUrl;
|
||||
}
|
||||
}
|
|
@ -92,7 +92,16 @@ export default class Markdown {
|
|||
}
|
||||
|
||||
toHTML() {
|
||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
const renderer = new commonmark.HtmlRenderer({
|
||||
safe: false,
|
||||
|
||||
// Set soft breaks to hard HTML breaks: commonmark
|
||||
// puts softbreaks in for multiple lines in a blockquote,
|
||||
// so if these are just newline characters then the
|
||||
// block quote ends up all on one line
|
||||
// (https://github.com/vector-im/riot-web/issues/3154)
|
||||
softbreak: '<br />',
|
||||
});
|
||||
const real_paragraph = renderer.paragraph;
|
||||
|
||||
renderer.paragraph = function(node, entering) {
|
||||
|
|
|
@ -50,6 +50,18 @@ class MatrixClientPeg {
|
|||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
this.indexedDbWorkerScript = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the script href passed to the IndexedDB web worker
|
||||
* If set, a separate web worker will be started to run the IndexedDB
|
||||
* queries on.
|
||||
*
|
||||
* @param {string} script href to the script to be passed to the web worker
|
||||
*/
|
||||
setIndexedDbWorkerScript(script) {
|
||||
this.indexedDbWorkerScript = script;
|
||||
}
|
||||
|
||||
get(): MatrixClient {
|
||||
|
@ -122,12 +134,15 @@ class MatrixClientPeg {
|
|||
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
||||
}
|
||||
if (window.indexedDB && localStorage) {
|
||||
opts.store = new Matrix.IndexedDBStore(
|
||||
new Matrix.IndexedDBStoreBackend(window.indexedDB),
|
||||
new Matrix.SyncAccumulator(), {
|
||||
localStorage: localStorage,
|
||||
}
|
||||
);
|
||||
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
||||
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
||||
|
||||
opts.store = new Matrix.IndexedDBStore({
|
||||
indexedDB: window.indexedDB,
|
||||
dbName: "riot-web-sync",
|
||||
localStorage: localStorage,
|
||||
workerScript: this.indexedDbWorkerScript,
|
||||
});
|
||||
}
|
||||
|
||||
this.matrixClient = Matrix.createClient(opts);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var PlatformPeg = require("./PlatformPeg");
|
||||
var TextForEvent = require('./TextForEvent');
|
||||
|
@ -99,16 +98,16 @@ var Notifier = {
|
|||
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||
this.toolbarHidden = false;
|
||||
this.isPrepared = false;
|
||||
this.isSyncing = false;
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||
}
|
||||
this.isPrepared = false;
|
||||
this.isSyncing = false;
|
||||
},
|
||||
|
||||
supportsDesktopNotifications: function() {
|
||||
|
@ -214,18 +213,18 @@ var Notifier = {
|
|||
},
|
||||
|
||||
onSyncStateChange: function(state) {
|
||||
if (state === "PREPARED" || state === "SYNCING") {
|
||||
this.isPrepared = true;
|
||||
if (state === "SYNCING") {
|
||||
this.isSyncing = true;
|
||||
}
|
||||
else if (state === "STOPPED" || state === "ERROR") {
|
||||
this.isPrepared = false;
|
||||
this.isSyncing = false;
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (toStartOfTimeline) return;
|
||||
if (!room) return;
|
||||
if (!this.isPrepared) 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 (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
|
||||
|
|
|
@ -18,11 +18,27 @@ var MatrixClientPeg = require('./MatrixClientPeg');
|
|||
var dis = require('./dispatcher');
|
||||
var sdk = require('./index');
|
||||
var Modal = require('./Modal');
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
|
||||
module.exports = {
|
||||
resendUnsentEvents: function(room) {
|
||||
room.getPendingEvents().filter(function(ev) {
|
||||
return ev.status === EventStatus.NOT_SENT;
|
||||
}).forEach(function(event) {
|
||||
module.exports.resend(event);
|
||||
});
|
||||
},
|
||||
cancelUnsentEvents: function(room) {
|
||||
room.getPendingEvents().filter(function(ev) {
|
||||
return ev.status === EventStatus.NOT_SENT;
|
||||
}).forEach(function(event) {
|
||||
module.exports.removeFromQueue(event);
|
||||
});
|
||||
},
|
||||
resend: function(event) {
|
||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
MatrixClientPeg.get().resendEvent(
|
||||
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
||||
event, room
|
||||
).done(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent',
|
||||
|
@ -33,16 +49,11 @@ module.exports = {
|
|||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
||||
onFinished: (r) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
},
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: room,
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
|
@ -51,7 +62,6 @@ module.exports = {
|
|||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeFromQueue: function(event) {
|
||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||
dis.dispatch({
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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.
|
||||
*/
|
||||
export const LEVEL_ROLE_MAP = {
|
||||
undefined: 'Default',
|
||||
0: 'User',
|
||||
50: 'Moderator',
|
||||
100: 'Admin',
|
||||
};
|
||||
|
||||
export function textualPowerLevel(level, userDefault) {
|
||||
if (LEVEL_ROLE_MAP[level]) {
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
|
||||
} else {
|
||||
return level;
|
||||
}
|
||||
}
|
14
src/Rooms.js
14
src/Rooms.js
|
@ -79,6 +79,20 @@ export function looksLikeDirectMessageRoom(room, me) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function guessAndSetDMRoom(room, isDirect) {
|
||||
let newTarget;
|
||||
if (isDirect) {
|
||||
const guessedTarget = guessDMRoomTarget(
|
||||
room, room.getMember(MatrixClientPeg.get().credentials.userId),
|
||||
);
|
||||
newTarget = guessedTarget.userId;
|
||||
} else {
|
||||
newTarget = null;
|
||||
}
|
||||
|
||||
return setDMRoom(room.roomId, newTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks or unmarks the given room as being as a DM room.
|
||||
* @param {string} roomId The ID of the room to modify
|
||||
|
|
465
src/Signup.js
465
src/Signup.js
|
@ -1,465 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var SignupStages = require("./SignupStages");
|
||||
var dis = require("./dispatcher");
|
||||
var q = require("q");
|
||||
var url = require("url");
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
|
||||
/**
|
||||
* A base class for common functionality between Registration and Login e.g.
|
||||
* storage of HS/IS URLs.
|
||||
*/
|
||||
class Signup {
|
||||
constructor(hsUrl, isUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
this._isUrl = isUrl;
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
return this._hsUrl;
|
||||
}
|
||||
|
||||
getIdentityServerUrl() {
|
||||
return this._isUrl;
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(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
|
||||
* This exists for the lifetime of a user's attempt to register an account,
|
||||
* so if their registration attempt fails for whatever reason and they
|
||||
* try again, call register() on the same instance again.
|
||||
*
|
||||
* TODO: parts of this overlap heavily with InteractiveAuth in the js-sdk. It
|
||||
* would be nice to make use of that rather than rolling our own version of it.
|
||||
*/
|
||||
class Register extends Signup {
|
||||
constructor(hsUrl, isUrl, opts) {
|
||||
super(hsUrl, isUrl, opts);
|
||||
this.setStep("START");
|
||||
this.data = null; // from the server
|
||||
// random other stuff (e.g. query params, NOT params from the server)
|
||||
this.params = {};
|
||||
this.credentials = null;
|
||||
this.activeStage = null;
|
||||
this.registrationPromise = null;
|
||||
// These values MUST be undefined else we'll send "username: null" which
|
||||
// will error on Synapse rather than having the key absent.
|
||||
this.username = undefined; // desired
|
||||
this.email = undefined; // desired
|
||||
this.password = undefined; // desired
|
||||
}
|
||||
|
||||
setClientSecret(secret) {
|
||||
this.params.clientSecret = secret;
|
||||
}
|
||||
|
||||
setSessionId(sessionId) {
|
||||
this.params.sessionId = sessionId;
|
||||
}
|
||||
|
||||
setRegistrationUrl(regUrl) {
|
||||
this.params.registrationUrl = regUrl;
|
||||
}
|
||||
|
||||
setIdSid(idSid) {
|
||||
this.params.idSid = idSid;
|
||||
}
|
||||
|
||||
setReferrer(referrer) {
|
||||
this.params.referrer = referrer;
|
||||
}
|
||||
|
||||
setGuestAccessToken(token) {
|
||||
this.guestAccessToken = token;
|
||||
}
|
||||
|
||||
getStep() {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
getServerData() {
|
||||
return this.data || {};
|
||||
}
|
||||
|
||||
getPromise() {
|
||||
return this.registrationPromise;
|
||||
}
|
||||
|
||||
setStep(step) {
|
||||
this._step = 'Register.' + step;
|
||||
// TODO:
|
||||
// It's a shame this is going to the global dispatcher, we only really
|
||||
// want things which have an instance of this class to be able to add
|
||||
// listeners...
|
||||
console.log("Dispatching 'registration_step_update' for step %s", this._step);
|
||||
dis.dispatch({
|
||||
action: "registration_step_update"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the registration process from the first stage
|
||||
*/
|
||||
register(formVals) {
|
||||
var {username, password, email} = formVals;
|
||||
this.email = email;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
const client = this._createTemporaryClient();
|
||||
this.activeStage = null;
|
||||
|
||||
// If there hasn't been a client secret set by this point,
|
||||
// generate one for this session. It will only be used if
|
||||
// we do email verification, but far simpler to just make
|
||||
// sure we have one.
|
||||
// We re-use this same secret over multiple calls to register
|
||||
// so that the identity server can honour the sendAttempt
|
||||
// parameter and not re-send email unless we actually want
|
||||
// another mail to be sent.
|
||||
if (!this.params.clientSecret) {
|
||||
this.params.clientSecret = client.generateClientSecret();
|
||||
}
|
||||
return this._tryRegister(client);
|
||||
}
|
||||
|
||||
_tryRegister(client, authDict, poll_for_success) {
|
||||
var self = this;
|
||||
|
||||
var bindEmail;
|
||||
|
||||
if (this.username && this.password) {
|
||||
// only need to bind_email when sending u/p - sending it at other
|
||||
// times clobbers the u/p resulting in M_MISSING_PARAM (password)
|
||||
bindEmail = true;
|
||||
}
|
||||
|
||||
// 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.guestAccessToken
|
||||
).then(function(result) {
|
||||
self.credentials = result;
|
||||
self.setStep("COMPLETE");
|
||||
return result; // contains the credentials
|
||||
}, function(error) {
|
||||
if (error.httpStatus === 401) {
|
||||
if (error.data && error.data.flows) {
|
||||
// Remember the session ID from the server:
|
||||
// Either this is our first 401 in which case we need to store the
|
||||
// session ID for future calls, or it isn't in which case this
|
||||
// is just a no-op since it ought to be the same (or if it isn't,
|
||||
// we should use the latest one from the server in any case).
|
||||
self.params.sessionId = error.data.session;
|
||||
self.data = error.data || {};
|
||||
var flow = self.chooseFlow(error.data.flows);
|
||||
|
||||
if (flow) {
|
||||
console.log("Active flow => %s", JSON.stringify(flow));
|
||||
var flowStage = self.firstUncompletedStage(flow);
|
||||
if (!self.activeStage || flowStage != self.activeStage.type) {
|
||||
return self._startStage(client, flowStage).catch(function(err) {
|
||||
self.setStep('START');
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (poll_for_success) {
|
||||
return q.delay(2000).then(function() {
|
||||
return self._tryRegister(client, authDict, poll_for_success);
|
||||
});
|
||||
} else {
|
||||
throw new Error("Authorisation failed!");
|
||||
}
|
||||
} else {
|
||||
if (error.errcode === 'M_USER_IN_USE') {
|
||||
throw new Error("Username in use");
|
||||
} else if (error.errcode == 'M_INVALID_USERNAME') {
|
||||
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
|
||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||
let msg = null;
|
||||
if (error.message) {
|
||||
msg = error.message;
|
||||
} else if (error.errcode) {
|
||||
msg = error.errcode;
|
||||
}
|
||||
if (msg) {
|
||||
throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`);
|
||||
} else {
|
||||
throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`);
|
||||
}
|
||||
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
||||
throw new Error(
|
||||
`Server error during registration! (${error.httpStatus})`
|
||||
);
|
||||
} else if (error.name == "M_MISSING_PARAM") {
|
||||
// The HS hasn't remembered the login params from
|
||||
// the first try when the login email was sent.
|
||||
throw new Error(
|
||||
"This home server does not support resuming registration."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
firstUncompletedStage(flow) {
|
||||
for (var i = 0; i < flow.stages.length; ++i) {
|
||||
if (!this.hasCompletedStage(flow.stages[i])) {
|
||||
return flow.stages[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasCompletedStage(stageType) {
|
||||
var completed = (this.data || {}).completed || [];
|
||||
return completed.indexOf(stageType) !== -1;
|
||||
}
|
||||
|
||||
_startStage(client, stageName) {
|
||||
var self = this;
|
||||
this.setStep(`STEP_${stageName}`);
|
||||
var StageClass = SignupStages[stageName];
|
||||
if (!StageClass) {
|
||||
// no idea how to handle this!
|
||||
throw new Error("Unknown stage: " + stageName);
|
||||
}
|
||||
|
||||
var stage = new StageClass(client, this);
|
||||
this.activeStage = stage;
|
||||
return stage.complete().then(function(request) {
|
||||
if (request.auth) {
|
||||
console.log("Stage %s is returning an auth dict", stageName);
|
||||
return self._tryRegister(client, request.auth, request.poll_for_success);
|
||||
}
|
||||
else {
|
||||
// never resolve the promise chain. This is for things like email auth
|
||||
// which display a "check your email" message and relies on the
|
||||
// link in the email to actually register you.
|
||||
console.log("Waiting for external action.");
|
||||
return q.defer().promise;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chooseFlow(flows) {
|
||||
// If the user gave us an email then we want to pick an email
|
||||
// flow we can do, else any other flow.
|
||||
var emailFlow = null;
|
||||
var otherFlow = null;
|
||||
flows.forEach(function(flow) {
|
||||
var flowHasEmail = false;
|
||||
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
|
||||
var stage = flow.stages[stageI];
|
||||
|
||||
if (!SignupStages[stage]) {
|
||||
// we can't do this flow, don't have a Stage impl.
|
||||
return;
|
||||
}
|
||||
|
||||
if (stage === EMAIL_STAGE_TYPE) {
|
||||
flowHasEmail = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (flowHasEmail) {
|
||||
emailFlow = flow;
|
||||
} else {
|
||||
otherFlow = flow;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) {
|
||||
// we've been given an email or we've already done an email part
|
||||
return emailFlow;
|
||||
} else {
|
||||
return otherFlow;
|
||||
}
|
||||
}
|
||||
|
||||
recheckState() {
|
||||
// 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
|
||||
// to know this though. A better solution would be to ask the stages if
|
||||
// they are ready to do something rather than accepting that we know about
|
||||
// email auth and its internals.
|
||||
this.params.hasEmailInfo = (
|
||||
this.params.clientSecret && this.params.sessionId && this.params.idSid
|
||||
);
|
||||
|
||||
if (this.params.hasEmailInfo) {
|
||||
const client = this._createTemporaryClient();
|
||||
this.registrationPromise = this._startStage(client, EMAIL_STAGE_TYPE);
|
||||
}
|
||||
return this.registrationPromise;
|
||||
}
|
||||
|
||||
tellStage(stageName, data) {
|
||||
if (this.activeStage && this.activeStage.type === stageName) {
|
||||
console.log("Telling stage %s about something..", stageName);
|
||||
this.activeStage.onReceiveData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Login extends Signup {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
super(hsUrl, isUrl, opts);
|
||||
this._fallbackHsUrl = fallbackHsUrl;
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
}
|
||||
|
||||
getFlows() {
|
||||
var self = this;
|
||||
var client = this._createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
// technically the UI should display options for all flows for the
|
||||
// user to then choose one, so return all the flows here.
|
||||
return self._flows;
|
||||
});
|
||||
}
|
||||
|
||||
chooseFlow(flowIndex) {
|
||||
this._currentFlowIndex = flowIndex;
|
||||
}
|
||||
|
||||
getCurrentFlowStep() {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login so we can ignore it.
|
||||
var flowStep = this._flows[this._currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginAsGuest() {
|
||||
var client = this._createTemporaryClient();
|
||||
return client.registerGuest({
|
||||
body: {
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
},
|
||||
}).then((creds) => {
|
||||
return {
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: this._hsUrl,
|
||||
identityServerUrl: this._isUrl,
|
||||
guest: true
|
||||
};
|
||||
}, (error) => {
|
||||
if (error.httpStatus === 403) {
|
||||
error.friendlyText = "Guest access is disabled on this Home Server.";
|
||||
} else {
|
||||
error.friendlyText = "Failed to register as guest: " + error.data;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
loginViaPassword(username, pass) {
|
||||
var self = this;
|
||||
var isEmail = username.indexOf("@") > 0;
|
||||
var loginParams = {
|
||||
password: pass,
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
};
|
||||
if (isEmail) {
|
||||
loginParams.medium = 'email';
|
||||
loginParams.address = username;
|
||||
} else {
|
||||
loginParams.user = username;
|
||||
}
|
||||
|
||||
var client = this._createTemporaryClient();
|
||||
return client.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
});
|
||||
}, function(error) {
|
||||
if (error.httpStatus == 400 && loginParams.medium) {
|
||||
error.friendlyText = (
|
||||
'This Home Server does not support login using email address.'
|
||||
);
|
||||
}
|
||||
else if (error.httpStatus === 403) {
|
||||
error.friendlyText = (
|
||||
'Incorrect username and/or password.'
|
||||
);
|
||||
if (self._fallbackHsUrl) {
|
||||
var fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
});
|
||||
}, function(fallback_error) {
|
||||
// throw the original error
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
error.friendlyText = (
|
||||
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
redirectToCas() {
|
||||
var client = this._createTemporaryClient();
|
||||
var parsedUrl = url.parse(window.location.href, true);
|
||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
||||
window.location.href = casUrl;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.Register = Register;
|
||||
module.exports.Login = Login;
|
|
@ -1,177 +0,0 @@
|
|||
"use strict";
|
||||
var q = require("q");
|
||||
|
||||
/**
|
||||
* An interface class which login types should abide by.
|
||||
*/
|
||||
class Stage {
|
||||
constructor(type, matrixClient, signupInstance) {
|
||||
this.type = type;
|
||||
this.client = matrixClient;
|
||||
this.signupInstance = signupInstance;
|
||||
}
|
||||
|
||||
complete() {
|
||||
// Return a promise which is:
|
||||
// RESOLVED => With an Object which has an 'auth' key which is the auth dict
|
||||
// to submit.
|
||||
// REJECTED => With an Error if there was a problem with this stage.
|
||||
// Has a "message" string and an "isFatal" flag.
|
||||
return q.reject("NOT IMPLEMENTED");
|
||||
}
|
||||
|
||||
onReceiveData() {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
Stage.TYPE = "NOT IMPLEMENTED";
|
||||
|
||||
|
||||
/**
|
||||
* This stage requires no auth.
|
||||
*/
|
||||
class DummyStage extends Stage {
|
||||
constructor(matrixClient, signupInstance) {
|
||||
super(DummyStage.TYPE, matrixClient, signupInstance);
|
||||
}
|
||||
|
||||
complete() {
|
||||
return q({
|
||||
auth: {
|
||||
type: DummyStage.TYPE
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
DummyStage.TYPE = "m.login.dummy";
|
||||
|
||||
|
||||
/**
|
||||
* This stage uses Google's Recaptcha to do auth.
|
||||
*/
|
||||
class RecaptchaStage extends Stage {
|
||||
constructor(matrixClient, signupInstance) {
|
||||
super(RecaptchaStage.TYPE, matrixClient, signupInstance);
|
||||
this.authDict = {
|
||||
auth: {
|
||||
type: 'm.login.recaptcha',
|
||||
// we'll add in the response param if we get one from the local user.
|
||||
},
|
||||
poll_for_success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// called when the recaptcha has been completed.
|
||||
onReceiveData(data) {
|
||||
if (!data || !data.response) {
|
||||
return;
|
||||
}
|
||||
this.authDict.auth.response = data.response;
|
||||
}
|
||||
|
||||
complete() {
|
||||
// we return the authDict with no response, telling Signup to keep polling
|
||||
// the server in case the captcha is filled in on another window (e.g. by
|
||||
// following a nextlink from an email signup). If the user completes the
|
||||
// captcha locally, then we return at the next poll.
|
||||
return q(this.authDict);
|
||||
}
|
||||
}
|
||||
RecaptchaStage.TYPE = "m.login.recaptcha";
|
||||
|
||||
|
||||
/**
|
||||
* This state uses the IS to verify email addresses.
|
||||
*/
|
||||
class EmailIdentityStage extends Stage {
|
||||
constructor(matrixClient, signupInstance) {
|
||||
super(EmailIdentityStage.TYPE, matrixClient, signupInstance);
|
||||
}
|
||||
|
||||
_completeVerify() {
|
||||
// pull out the host of the IS URL by creating an anchor element
|
||||
var isLocation = document.createElement('a');
|
||||
isLocation.href = this.signupInstance.getIdentityServerUrl();
|
||||
|
||||
var clientSecret = this.clientSecret || this.signupInstance.params.clientSecret;
|
||||
var sid = this.sid || this.signupInstance.params.idSid;
|
||||
|
||||
return q({
|
||||
auth: {
|
||||
type: 'm.login.email.identity',
|
||||
threepid_creds: {
|
||||
sid: sid,
|
||||
client_secret: clientSecret,
|
||||
id_server: isLocation.host
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the email stage.
|
||||
*
|
||||
* This is called twice under different circumstances:
|
||||
* 1) When requesting an email token from the IS
|
||||
* 2) When validating query parameters received from the link in the email
|
||||
*/
|
||||
complete() {
|
||||
// TODO: The Registration class shouldn't really know this info.
|
||||
if (this.signupInstance.params.hasEmailInfo) {
|
||||
return this._completeVerify();
|
||||
}
|
||||
|
||||
this.clientSecret = this.signupInstance.params.clientSecret;
|
||||
if (!this.clientSecret) {
|
||||
return q.reject(new Error("No client secret specified by Signup class!"));
|
||||
}
|
||||
|
||||
var nextLink = this.signupInstance.params.registrationUrl +
|
||||
'?client_secret=' +
|
||||
encodeURIComponent(this.clientSecret) +
|
||||
"&hs_url=" +
|
||||
encodeURIComponent(this.signupInstance.getHomeserverUrl()) +
|
||||
"&is_url=" +
|
||||
encodeURIComponent(this.signupInstance.getIdentityServerUrl()) +
|
||||
"&session_id=" +
|
||||
encodeURIComponent(this.signupInstance.getServerData().session);
|
||||
|
||||
// Add the user ID of the referring user, if set
|
||||
if (this.signupInstance.params.referrer) {
|
||||
nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this.client.requestRegisterEmailToken(
|
||||
this.signupInstance.email,
|
||||
this.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
nextLink
|
||||
).then(function(response) {
|
||||
self.sid = response.sid;
|
||||
self.signupInstance.setIdSid(self.sid);
|
||||
return self._completeVerify();
|
||||
}).then(function(request) {
|
||||
request.poll_for_success = true;
|
||||
return request;
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
var e = {
|
||||
isFatal: true
|
||||
};
|
||||
if (error.errcode == 'M_THREEPID_IN_USE') {
|
||||
e.message = "This email address is already registered";
|
||||
} else {
|
||||
e.message = 'Unable to contact the given identity server';
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
EmailIdentityStage.TYPE = "m.login.email.identity";
|
||||
|
||||
module.exports = {
|
||||
[DummyStage.TYPE]: DummyStage,
|
||||
[RecaptchaStage.TYPE]: RecaptchaStage,
|
||||
[EmailIdentityStage.TYPE]: EmailIdentityStage
|
||||
};
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var CallHandler = require("./CallHandler");
|
||||
|
||||
import * as Roles from './Roles';
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
|
@ -116,7 +118,6 @@ function textForRoomNameEvent(ev) {
|
|||
|
||||
function textForMessageEvent(ev) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
|
@ -183,6 +184,45 @@ function textForEncryptionEvent(event) {
|
|||
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")";
|
||||
}
|
||||
|
||||
// Currently will only display a change if a user's power level is changed
|
||||
function textForPowerEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
if (!event.getPrevContent() || !event.getPrevContent().users) {
|
||||
return '';
|
||||
}
|
||||
const userDefault = event.getContent().users_default || 0;
|
||||
// Construct set of userIds
|
||||
let users = [];
|
||||
Object.keys(event.getContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
);
|
||||
Object.keys(event.getPrevContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
);
|
||||
let diff = [];
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
const from = event.getPrevContent().users[userId];
|
||||
// Current power level
|
||||
const to = event.getContent().users[userId];
|
||||
if (to !== from) {
|
||||
diff.push(
|
||||
userId +
|
||||
' from ' + Roles.textualPowerLevel(from, userDefault) +
|
||||
' to ' + Roles.textualPowerLevel(to, userDefault)
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!diff.length) {
|
||||
return '';
|
||||
}
|
||||
return senderName + ' changed the power level of ' + diff.join(', ');
|
||||
}
|
||||
|
||||
var handlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
|
@ -194,6 +234,7 @@ var handlers = {
|
|||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.encryption': textForEncryptionEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 dis from './dispatcher';
|
||||
import sdk from './index';
|
||||
import Modal from './Modal';
|
||||
|
||||
let isDialogOpen = false;
|
||||
|
||||
const onAction = function(payload) {
|
||||
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
isDialogOpen = true;
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: payload.err.devices,
|
||||
room: payload.room,
|
||||
onFinished: (r) => {
|
||||
isDialogOpen = false;
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
},
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
}
|
||||
}
|
||||
|
||||
let ref = null;
|
||||
|
||||
export function startListening () {
|
||||
ref = dis.register(onAction);
|
||||
}
|
||||
|
||||
export function stopListening () {
|
||||
if (ref) {
|
||||
dis.unregister(ref);
|
||||
ref = null;
|
||||
}
|
||||
}
|
|
@ -75,8 +75,12 @@ import views$create_room$RoomAlias from './components/views/create_room/RoomAlia
|
|||
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
||||
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
|
||||
import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
|
||||
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
|
||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||
import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
|
||||
views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
|
||||
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
|
||||
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
||||
|
@ -107,6 +111,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi
|
|||
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
||||
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
||||
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
||||
import views$elements$Dropdown from './components/views/elements/Dropdown';
|
||||
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
|
||||
import views$elements$EditableText from './components/views/elements/EditableText';
|
||||
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
||||
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
||||
|
@ -129,6 +135,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm';
|
|||
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
||||
import views$login$CasLogin from './components/views/login/CasLogin';
|
||||
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
||||
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
|
||||
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
|
||||
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
||||
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
||||
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
||||
|
@ -221,6 +229,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread
|
|||
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
|
||||
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
||||
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
|
||||
import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
|
||||
views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
|
||||
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
|
||||
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
||||
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
||||
|
|
|
@ -27,6 +27,9 @@ export default React.createClass({
|
|||
displayName: 'InteractiveAuth',
|
||||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
|
@ -38,11 +41,34 @@ export default React.createClass({
|
|||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the auth process has finished
|
||||
// callback called when the auth process has finished,
|
||||
// successfully or unsuccessfully.
|
||||
// @param {bool} status True if the operation requiring
|
||||
// auth was completed sucessfully, false if canceled.
|
||||
// @param result The result of the authenticated call
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// @param {object} result The result of the authenticated call
|
||||
// if successful, otherwise the error object
|
||||
// @param {object} extra Additional information about the UI Auth
|
||||
// process:
|
||||
// * emailSid {string} If email auth was performed, the sid of
|
||||
// the auth session.
|
||||
// * clientSecret {string} The client secret used in auth
|
||||
// sessions with the ID server.
|
||||
onAuthFinished: React.PropTypes.func.isRequired,
|
||||
|
||||
// Inputs provided by the user to the auth process
|
||||
// and used by various stages. As passed to js-sdk
|
||||
// interactive-auth
|
||||
inputs: React.PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: React.PropTypes.func,
|
||||
sessionId: React.PropTypes.string,
|
||||
clientSecret: React.PropTypes.string,
|
||||
emailSid: React.PropTypes.string,
|
||||
|
||||
// If true, poll to see if the auth flow has been completed
|
||||
// out-of-band
|
||||
poll: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -60,12 +86,22 @@ export default React.createClass({
|
|||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
startAuthStage: this._startAuthStage,
|
||||
inputs: this.props.inputs,
|
||||
stateUpdated: this._authStateUpdated,
|
||||
matrixClient: this.props.matrixClient,
|
||||
sessionId: this.props.sessionId,
|
||||
clientSecret: this.props.clientSecret,
|
||||
emailSid: this.props.emailSid,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
this.props.onFinished(true, result);
|
||||
const extra = {
|
||||
emailSid: this._authLogic.getEmailSid(),
|
||||
clientSecret: this._authLogic.getClientSecret(),
|
||||
};
|
||||
this.props.onAuthFinished(true, result, extra);
|
||||
}).catch((error) => {
|
||||
this.props.onAuthFinished(false, error);
|
||||
console.error("Error during user-interactive auth:", error);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
|
@ -76,26 +112,48 @@ export default React.createClass({
|
|||
errorText: msg
|
||||
});
|
||||
}).done();
|
||||
|
||||
this._intervalId = null;
|
||||
if (this.props.poll) {
|
||||
this._intervalId = setInterval(() => {
|
||||
this._authLogic.poll();
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
|
||||
if (this._intervalId !== null) {
|
||||
clearInterval(this._intervalId);
|
||||
}
|
||||
},
|
||||
|
||||
_startAuthStage: function(stageType, error) {
|
||||
_authStateUpdated: function(stageType, stageState) {
|
||||
const oldStage = this.state.authStage;
|
||||
this.setState({
|
||||
authStage: stageType,
|
||||
errorText: error ? error.error : null,
|
||||
}, this._setFocus);
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
}, () => {
|
||||
if (oldStage != stageType) this._setFocus();
|
||||
});
|
||||
},
|
||||
|
||||
_requestCallback: function(auth) {
|
||||
_requestCallback: function(auth, background) {
|
||||
const makeRequestPromise = this.props.makeRequest(auth);
|
||||
|
||||
// if it's a background request, just do it: we don't want
|
||||
// it to affect the state of our UI.
|
||||
if (background) return makeRequestPromise;
|
||||
|
||||
// otherwise, manage the state of the spinner and error messages
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
return makeRequestPromise.finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -117,19 +175,35 @@ export default React.createClass({
|
|||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
var StageComponent = getEntryComponentForLoginType(stage);
|
||||
if (!stage) return null;
|
||||
|
||||
const StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
loginType={stage}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
clientSecret={this._authLogic.getClientSecret()}
|
||||
stageParams={this._authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this._submitAuthDict}
|
||||
errorText={this.state.stageErrorText}
|
||||
busy={this.state.busy}
|
||||
inputs={this.props.inputs}
|
||||
stageState={this.state.stageState}
|
||||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
_onAuthStageFailed: function(e) {
|
||||
this.props.onAuthFinished(false, e);
|
||||
},
|
||||
_setEmailSid: function(sid) {
|
||||
this._authLogic.setEmailSid(sid);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
|
|
|
@ -49,11 +49,16 @@ export default React.createClass({
|
|||
|
||||
childContextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
authCache: {
|
||||
auth: {},
|
||||
lastUpdate: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -76,6 +81,13 @@ export default React.createClass({
|
|||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.roomView.canResetTimeline();
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -94,6 +106,17 @@ export default React.createClass({
|
|||
var handled = false;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
|
||||
// Implemented this way so possible handling for other pages is neater
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.UserSettings:
|
||||
this.props.onUserSettingsClose();
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,10 +29,6 @@ var UserActivity = require("../../UserActivity");
|
|||
var Presence = require("../../Presence");
|
||||
var dis = require("../../dispatcher");
|
||||
|
||||
var Login = require("./login/Login");
|
||||
var Registration = require("./login/Registration");
|
||||
var PostRegistration = require("./login/PostRegistration");
|
||||
|
||||
var Modal = require("../../Modal");
|
||||
var Tinter = require("../../Tinter");
|
||||
var sdk = require('../../index');
|
||||
|
@ -41,6 +38,7 @@ var Lifecycle = require('../../Lifecycle');
|
|||
var PageTypes = require('../../PageTypes');
|
||||
|
||||
var createRoom = require("../../createRoom");
|
||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MatrixChat',
|
||||
|
@ -61,9 +59,19 @@ module.exports = React.createClass({
|
|||
// called when the session load completes
|
||||
onLoadCompleted: React.PropTypes.func,
|
||||
|
||||
// Represents the screen to display as a result of parsing the initial
|
||||
// window.location
|
||||
initialScreenAfterLogin: React.PropTypes.shape({
|
||||
screen: React.PropTypes.string.isRequired,
|
||||
params: React.PropTypes.object,
|
||||
}),
|
||||
|
||||
// displayname, if any, to set on the device when logging
|
||||
// in/registering.
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
|
||||
// A function that makes a registration URL
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
|
@ -84,6 +92,12 @@ module.exports = React.createClass({
|
|||
var s = {
|
||||
loading: true,
|
||||
screen: undefined,
|
||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
||||
|
||||
// Stashed guest credentials if the user logs out
|
||||
// whilst logged in as a guest user (so they can change
|
||||
// their mind & log back in)
|
||||
guestCreds: null,
|
||||
|
||||
// What the LoggedInView would be showing if visible
|
||||
page_type: null,
|
||||
|
@ -99,7 +113,8 @@ module.exports = React.createClass({
|
|||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||
viewUserId: null,
|
||||
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
loggingIn: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
ready: false,
|
||||
|
@ -179,13 +194,9 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
// Stashed guest credentials if the user logs out
|
||||
// whilst logged in as a guest user (so they can change
|
||||
// their mind & log back in)
|
||||
this.guestCreds = null;
|
||||
|
||||
// if the automatic session load failed, the error
|
||||
this.sessionLoadError = null;
|
||||
// Used by _viewRoom before getting state from sync
|
||||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = q.defer();
|
||||
|
||||
if (this.props.config.sync_timeline_limit) {
|
||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
|
@ -226,10 +237,20 @@ module.exports = React.createClass({
|
|||
if (this._teamToken) {
|
||||
console.info(`Team token set to ${this._teamToken}`);
|
||||
}
|
||||
|
||||
// Set a default HS with query param `hs_url`
|
||||
const paramHs = this.props.startingFragmentQueryParams.hs_url;
|
||||
if (paramHs) {
|
||||
console.log('Setting register_hs_url ', paramHs);
|
||||
this.setState({
|
||||
register_hs_url: paramHs,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
UDEHandler.startListening();
|
||||
|
||||
this.focusComposer = false;
|
||||
window.addEventListener("focus", this.onFocus);
|
||||
|
@ -265,7 +286,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
}).catch((e) => {
|
||||
console.error("Unable to load session", e);
|
||||
this.sessionLoadError = e.message;
|
||||
}).done(()=>{
|
||||
// stuff this through the dispatcher so that it happens
|
||||
// after the on_logged_in action.
|
||||
|
@ -276,6 +296,7 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
Lifecycle.stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
UDEHandler.stopListening();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
@ -291,7 +312,7 @@ module.exports = React.createClass({
|
|||
const newState = {
|
||||
screen: undefined,
|
||||
viewUserId: null,
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
ready: false,
|
||||
upgradeUsername: null,
|
||||
guestAccessToken: null,
|
||||
|
@ -301,88 +322,123 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
var self = this;
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
}
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'start_registration':
|
||||
var newState = payload.params || {};
|
||||
newState.screen = 'register';
|
||||
if (
|
||||
payload.params &&
|
||||
payload.params.client_secret &&
|
||||
payload.params.session_id &&
|
||||
payload.params.hs_url &&
|
||||
payload.params.is_url &&
|
||||
payload.params.sid
|
||||
) {
|
||||
newState.register_client_secret = payload.params.client_secret;
|
||||
newState.register_session_id = payload.params.session_id;
|
||||
newState.register_hs_url = payload.params.hs_url;
|
||||
newState.register_is_url = payload.params.is_url;
|
||||
newState.register_id_sid = payload.params.sid;
|
||||
}
|
||||
this.setStateForNewScreen(newState);
|
||||
const params = payload.params || {};
|
||||
this.setStateForNewScreen({
|
||||
screen: 'register',
|
||||
// these params may be undefined, but if they are,
|
||||
// unset them from our state: we don't want to
|
||||
// resume a previous registration session if the
|
||||
// user just clicked 'register'
|
||||
register_client_secret: params.client_secret,
|
||||
register_session_id: params.session_id,
|
||||
register_hs_url: params.hs_url,
|
||||
register_is_url: params.is_url,
|
||||
register_id_sid: params.sid,
|
||||
});
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_login':
|
||||
if (this.state.logged_in) return;
|
||||
if (MatrixClientPeg.get() &&
|
||||
MatrixClientPeg.get().isGuest()
|
||||
) {
|
||||
this.setState({
|
||||
guestCreds: MatrixClientPeg.getCredentials(),
|
||||
});
|
||||
}
|
||||
this.setStateForNewScreen({
|
||||
screen: 'login',
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({ // don't clobber logged_in status
|
||||
this.setState({ // don't clobber loggedIn status
|
||||
screen: 'post_registration'
|
||||
});
|
||||
break;
|
||||
case 'start_upgrade_registration':
|
||||
// stash our guest creds so we can backout if needed
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
// also stash our credentials, then if we restore the session,
|
||||
// we can just do it the same way whether we started upgrade
|
||||
// registration or explicitly logged out
|
||||
this.setStateForNewScreen({
|
||||
guestCreds: MatrixClientPeg.getCredentials(),
|
||||
screen: "register",
|
||||
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
|
||||
});
|
||||
|
||||
// stop the client: if we are syncing whilst the registration
|
||||
// is completed in another browser, we'll be 401ed for using
|
||||
// a guest access token for a non-guest account.
|
||||
// It will be restarted in onReturnToGuestClick
|
||||
Lifecycle.stopMatrixClient();
|
||||
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
if (this.state.logged_in) return;
|
||||
if (this.state.loggedIn) return;
|
||||
this.setStateForNewScreen({
|
||||
screen: 'forgot_password',
|
||||
});
|
||||
this.notifyNewScreen('forgot_password');
|
||||
break;
|
||||
case 'leave_room':
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
var roomId = payload.room_id;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Leave room",
|
||||
description: "Are you sure you want to leave the room?",
|
||||
onFinished: function(should_leave) {
|
||||
onFinished: (should_leave) => {
|
||||
if (should_leave) {
|
||||
var d = MatrixClientPeg.get().leave(roomId);
|
||||
const d = MatrixClientPeg.get().leave(payload.room_id);
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.then(function() {
|
||||
d.then(() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}, function(err) {
|
||||
if (this.currentRoomId === payload.room_id) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + payload.room_id + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to leave room",
|
||||
description: "Server may be unavailable, overloaded, or you hit a bug."
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'reject_invite':
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Reject invitation",
|
||||
description: "Are you sure you want to reject the invitation?",
|
||||
onFinished: (confirm) => {
|
||||
if (confirm) {
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
MatrixClientPeg.get().leave(payload.room_id).done(() => {
|
||||
modal.close();
|
||||
if (this.currentRoomId === payload.room_id) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to reject invitation",
|
||||
description: err.toString()
|
||||
});
|
||||
});
|
||||
|
@ -509,8 +565,11 @@ module.exports = React.createClass({
|
|||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
case 'on_logging_in':
|
||||
this.setState({loggingIn: true});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
this._onLoggedIn();
|
||||
this._onLoggedIn(payload.teamToken);
|
||||
break;
|
||||
case 'on_logged_out':
|
||||
this._onLoggedOut();
|
||||
|
@ -582,36 +641,38 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (this.sdkReady) {
|
||||
// if the SDK is not ready yet, remember what room
|
||||
// we're supposed to be on but don't notify about
|
||||
// the new screen yet (we won't be showing it yet)
|
||||
// The normal case where this happens is navigating
|
||||
// to the room in the URL bar on page load.
|
||||
var presentedId = room_info.room_alias || room_info.room_id;
|
||||
var room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
||||
// Wait for the first sync to complete so that if a room does have an alias,
|
||||
// it would have been retrieved.
|
||||
let waitFor = q(null);
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a room before first sync. room_id:', room_info.room_id);
|
||||
return;
|
||||
}
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
waitFor.done(() => {
|
||||
let presentedId = room_info.room_alias || room_info.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
||||
if (room) {
|
||||
var theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
const theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
|
||||
// No need to do this given RoomView triggers it itself...
|
||||
// var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
// var color_scheme = {};
|
||||
// if (color_scheme_event) {
|
||||
// color_scheme = color_scheme_event.getContent();
|
||||
// // XXX: we should validate the event
|
||||
// }
|
||||
// console.log("Tinter.tint from _viewRoom");
|
||||
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
// persist which room is being stored across refreshes and browser quits.
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_last_room_id', room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
if (room_info.event_id) {
|
||||
presentedId += "/"+room_info.event_id;
|
||||
presentedId += "/" + room_info.event_id;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
this.notifyNewScreen('room/' + presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
this.setState(newState);
|
||||
});
|
||||
},
|
||||
|
||||
_createChat: function() {
|
||||
|
@ -637,6 +698,14 @@ module.exports = React.createClass({
|
|||
_onLoadCompleted: function() {
|
||||
this.props.onLoadCompleted();
|
||||
this.setState({loading: false});
|
||||
|
||||
// Show screens (like 'register') that need to be shown without _onLoggedIn
|
||||
// being called. 'register' needs to be routed here when the email confirmation
|
||||
// link is clicked on.
|
||||
if (this.state.screenAfterLogin &&
|
||||
['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) {
|
||||
this._showScreenAfterLogin();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -686,13 +755,48 @@ module.exports = React.createClass({
|
|||
/**
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: function(credentials) {
|
||||
this.guestCreds = null;
|
||||
this.notifyNewScreen('');
|
||||
_onLoggedIn: function(teamToken) {
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
logged_in: true,
|
||||
guestCreds: null,
|
||||
loggedIn: true,
|
||||
loggingIn: false,
|
||||
});
|
||||
|
||||
if (teamToken) {
|
||||
// A team member has logged in, not a guest
|
||||
this._teamToken = teamToken;
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (this._is_registered) {
|
||||
// The user has just logged in after registering
|
||||
dis.dispatch({action: 'view_user_settings'});
|
||||
} else {
|
||||
this._showScreenAfterLogin();
|
||||
}
|
||||
},
|
||||
|
||||
_showScreenAfterLogin: function() {
|
||||
// If screenAfterLogin is set, use that, then null it so that a second login will
|
||||
// result in view_home_page, _user_settings or _room_directory
|
||||
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
|
||||
this.showScreen(
|
||||
this.state.screenAfterLogin.screen,
|
||||
this.state.screenAfterLogin.params
|
||||
);
|
||||
this.notifyNewScreen(this.state.screenAfterLogin.screen);
|
||||
this.setState({screenAfterLogin: null});
|
||||
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
|
||||
// Before defaulting to directory, show the last viewed room
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: localStorage.getItem('mx_last_room_id'),
|
||||
});
|
||||
} else if (this._teamToken) {
|
||||
// Team token might be set if we're a guest.
|
||||
// Guests do not call _onLoggedIn with a teamToken
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else {
|
||||
dis.dispatch({action: 'view_room_directory'});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -701,7 +805,7 @@ module.exports = React.createClass({
|
|||
_onLoggedOut: function() {
|
||||
this.notifyNewScreen('login');
|
||||
this.setStateForNewScreen({
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
|
@ -709,6 +813,7 @@ module.exports = React.createClass({
|
|||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
this._teamToken = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -716,9 +821,31 @@ module.exports = React.createClass({
|
|||
* (useful for setting listeners)
|
||||
*/
|
||||
_onWillStartClient() {
|
||||
var self = this;
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var self = this;
|
||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||
// memory consumed as the JS SDK stores multiple distinct copies of room
|
||||
// state (each of which can be 10s of MBs) for each DISJOINT timeline. This is
|
||||
// particularly noticeable when there are lots of 'limited' /sync responses
|
||||
// such as when laptops unsleep.
|
||||
// https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568
|
||||
cli.setCanResetTimelineCallback(function(roomId) {
|
||||
console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId);
|
||||
if (roomId !== self.state.currentRoomId) {
|
||||
// It is safe to remove events from rooms we are not viewing.
|
||||
return true;
|
||||
}
|
||||
// We are viewing the room which we want to reset. It is only safe to do
|
||||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self.refs.loggedInView) {
|
||||
return true;
|
||||
}
|
||||
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
self.updateStatusIndicator(state, prevState);
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
|
@ -726,55 +853,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
console.log("MatrixClient sync state => %s", state);
|
||||
if (state !== "PREPARED") { return; }
|
||||
self.sdkReady = true;
|
||||
|
||||
if (self.starting_room_alias_payload) {
|
||||
dis.dispatch(self.starting_room_alias_payload);
|
||||
delete self.starting_room_alias_payload;
|
||||
} else if (!self.state.page_type) {
|
||||
if (!self.state.currentRoomId) {
|
||||
var firstRoom = null;
|
||||
if (cli.getRooms() && cli.getRooms().length) {
|
||||
firstRoom = RoomListSorter.mostRecentActivityFirst(
|
||||
cli.getRooms()
|
||||
)[0].roomId;
|
||||
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
|
||||
} else {
|
||||
if (self._teamToken) {
|
||||
self.setState({ready: true, page_type: PageTypes.HomePage});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomView});
|
||||
}
|
||||
self.firstSyncComplete = true;
|
||||
self.firstSyncPromise.resolve();
|
||||
|
||||
// we notifyNewScreen now because now the room will actually be displayed,
|
||||
// and (mostly) now we can get the correct alias.
|
||||
var presentedId = self.state.currentRoomId;
|
||||
var room = MatrixClientPeg.get().getRoom(self.state.currentRoomId);
|
||||
if (room) {
|
||||
var theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
|
||||
if (presentedId != undefined) {
|
||||
self.notifyNewScreen('room/'+presentedId);
|
||||
} else {
|
||||
// There is no information on presentedId
|
||||
// so point user to fallback like /directory
|
||||
if (self._teamToken) {
|
||||
self.notifyNewScreen('home');
|
||||
} else {
|
||||
self.notifyNewScreen('directory');
|
||||
}
|
||||
}
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
} else {
|
||||
self.setState({ready: true});
|
||||
}
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
self.setState({ready: true});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
dis.dispatch({
|
||||
|
@ -874,12 +958,7 @@ module.exports = React.createClass({
|
|||
|
||||
// we can't view a room unless we're logged in
|
||||
// (a guest account is fine)
|
||||
if (!this.state.logged_in) {
|
||||
// we may still be loading (ie, trying to register a guest
|
||||
// session); otherwise we're (probably) already showing a login
|
||||
// screen. Either way, we'll show the room once the client starts.
|
||||
this.starting_room_alias_payload = payload;
|
||||
} else {
|
||||
if (this.state.loggedIn) {
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
} else if (screen.indexOf('user/') == 0) {
|
||||
|
@ -973,29 +1052,17 @@ module.exports = React.createClass({
|
|||
|
||||
onReturnToGuestClick: function() {
|
||||
// reanimate our guest login
|
||||
if (this.guestCreds) {
|
||||
Lifecycle.setLoggedIn(this.guestCreds);
|
||||
this.guestCreds = null;
|
||||
if (this.state.guestCreds) {
|
||||
Lifecycle.setLoggedIn(this.state.guestCreds);
|
||||
this.setState({guestCreds: null});
|
||||
}
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
// do post-registration stuff
|
||||
// This now goes straight to user settings
|
||||
// We use _setPage since if we wait for
|
||||
// showScreen to do the dispatch loop,
|
||||
// the showScreen dispatch will race with the
|
||||
// sdk sync finishing and we'll probably see
|
||||
// the page type still unset when the MatrixClient
|
||||
// is started and show the Room Directory instead.
|
||||
//this.showScreen("view_user_settings");
|
||||
this._setPage(PageTypes.UserSettings);
|
||||
},
|
||||
|
||||
onTeamMemberRegistered: function(teamToken) {
|
||||
onRegistered: function(credentials, teamToken) {
|
||||
// teamToken may not be truthy
|
||||
this._teamToken = teamToken;
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this._is_registered = true;
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
},
|
||||
|
||||
onFinishPostRegistration: function() {
|
||||
|
@ -1061,15 +1128,20 @@ module.exports = React.createClass({
|
|||
this.setState({currentRoomId: room_id});
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
if (this.props.startingFragmentQueryParams.referrer) {
|
||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||
}
|
||||
return this.props.makeRegistrationUrl(params);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
|
||||
// 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) {
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
// `loading` might be set to false before `loggedIn = true`, causing the default
|
||||
// (`<Login>`) to be visible for a few MS (say, whilst a request is in-flight to
|
||||
// the RTS). So in the meantime, use `loggingIn`, which is true between
|
||||
// actions `on_logging_in` and `on_logged_in`.
|
||||
if (this.state.loading || this.state.loggingIn) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
|
@ -1078,15 +1150,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
else if (this.state.screen == 'post_registration') {
|
||||
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
||||
return (
|
||||
<PostRegistration
|
||||
onComplete={this.onFinishPostRegistration} />
|
||||
);
|
||||
} else if (this.state.logged_in && this.state.ready) {
|
||||
} else if (this.state.loggedIn && this.state.ready) {
|
||||
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
||||
* we should go through and figure out what we actually need to pass down, as well
|
||||
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||
onRoomIdResolved={this.onRoomIdResolved}
|
||||
|
@ -1097,9 +1171,9 @@ module.exports = React.createClass({
|
|||
{...this.state}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.logged_in) {
|
||||
} else if (this.state.loggedIn) {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
|
@ -1109,6 +1183,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
const Registration = sdk.getComponent('structures.login.Registration');
|
||||
return (
|
||||
<Registration
|
||||
clientSecret={this.state.register_client_secret}
|
||||
|
@ -1124,16 +1199,16 @@ module.exports = React.createClass({
|
|||
teamServerConfig={this.props.config.teamServerConfig}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onTeamMemberRegistered={this.onTeamMemberRegistered}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.screen == 'forgot_password') {
|
||||
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
return (
|
||||
<ForgotPassword
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
|
@ -1145,7 +1220,8 @@ module.exports = React.createClass({
|
|||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
var r = (
|
||||
const Login = sdk.getComponent('structures.login.Login');
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
|
@ -1157,17 +1233,9 @@ module.exports = React.createClass({
|
|||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
initialErrorText={this.sessionLoadError}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
|
||||
// we only want to show the session load error the first time the
|
||||
// Login component is rendered. This is pretty hacky but I can't
|
||||
// think of another way to achieve it.
|
||||
this.sessionLoadError = null;
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -295,7 +295,10 @@ module.exports = React.createClass({
|
|||
var last = (i == lastShownEventIndex);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
|
||||
if (isMembershipChange(mxEv) &&
|
||||
EventTile.haveTileForEvent(mxEv) &&
|
||||
!mxEv.isRedacted()
|
||||
) {
|
||||
let ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
|
@ -349,7 +352,9 @@ module.exports = React.createClass({
|
|||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
data-scroll-token={eventId}>
|
||||
data-scroll-token={eventId}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
|
@ -362,10 +367,6 @@ module.exports = React.createClass({
|
|||
// replacing all of the DOM elements every time we paginate.
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
|
||||
prevEvent = mxEv;
|
||||
} else if (!mxEv.status) {
|
||||
// if we aren't showing the event, put in a dummy scroll token anyway, so
|
||||
// that we can scroll to the right place.
|
||||
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
||||
}
|
||||
|
||||
var isVisibleReadMarker = false;
|
||||
|
@ -410,7 +411,9 @@ module.exports = React.createClass({
|
|||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()) {
|
||||
continuation = true;
|
||||
|
@ -463,6 +466,7 @@ module.exports = React.createClass({
|
|||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-token={scrollToken}>
|
||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
onWidgetLoad={this._onWidgetLoad}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
|
@ -483,13 +487,17 @@ module.exports = React.createClass({
|
|||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare weekdays
|
||||
return prevEvent.getDate().getDay() !== nextEventDate.getDay();
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
|
|
|
@ -96,26 +96,12 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
|
||||
this._checkSize();
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
||||
this.props.onResize();
|
||||
}
|
||||
|
||||
const size = this._getSize(this.props, this.state);
|
||||
if (size > 0) {
|
||||
this.props.onVisible();
|
||||
} else {
|
||||
if (this.hideDebouncer) {
|
||||
clearTimeout(this.hideDebouncer);
|
||||
}
|
||||
this.hideDebouncer = setTimeout(() => {
|
||||
// temporarily stop hiding the statusbar as per
|
||||
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
|
||||
// this.props.onHidden();
|
||||
}, HIDE_DEBOUNCE_MS);
|
||||
}
|
||||
componentDidUpdate: function() {
|
||||
this._checkSize();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -142,31 +128,33 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||
_checkSize: function () {
|
||||
if (this.props.onVisible && this._getSize()) {
|
||||
this.props.onVisible();
|
||||
}
|
||||
},
|
||||
|
||||
// We don't need the actual height - just whether it is likely to have
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function(props, state) {
|
||||
if (state.syncState === "ERROR" ||
|
||||
(state.usersTyping.length > 0) ||
|
||||
props.numUnreadMessages ||
|
||||
!props.atEndOfLiveTimeline ||
|
||||
props.hasActiveCall) {
|
||||
_getSize: function() {
|
||||
if (this.state.syncState === "ERROR" ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall ||
|
||||
this.props.tabComplete.isTabCompleting()
|
||||
) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (props.tabCompleteEntries) {
|
||||
} else if (this.props.tabCompleteEntries) {
|
||||
return STATUS_BAR_HIDDEN;
|
||||
} else if (props.unsentMessageError) {
|
||||
} else if (this.props.unsentMessageError) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
},
|
||||
|
||||
// determine if we need to call onResize
|
||||
_checkForResize: function(prevProps, prevState) {
|
||||
// figure out the old height and the new height of the status bar.
|
||||
return this._getSize(prevProps, prevState)
|
||||
!== this._getSize(this.props, this.state);
|
||||
},
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
//
|
||||
// if wantPlaceholder is true, we include a "..." placeholder if
|
||||
|
@ -194,8 +182,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (this.props.hasActiveCall) {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return (
|
||||
<img src="img/sound-indicator.svg" width="23" height="20"/>
|
||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20"/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -490,13 +490,49 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.messagePanel.canResetTimeline();
|
||||
},
|
||||
|
||||
// called when state.room is first initialised (either at initial load,
|
||||
// after a successful peek, or after we join the room).
|
||||
_onRoomLoaded: function(room) {
|
||||
this._warnAboutEncryption(room);
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function (room) {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
let userHasUsedEncryption = false;
|
||||
if (localStorage) {
|
||||
userHasUsedEncryption = localStorage.getItem('mx_user_has_used_encryption');
|
||||
}
|
||||
if (!userHasUsedEncryption) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning!",
|
||||
hasCancelButton: false,
|
||||
description: (
|
||||
<div>
|
||||
<p>End-to-end encryption is in beta and may not be reliable.</p>
|
||||
<p>You should <b>not</b> yet trust it to secure data.</p>
|
||||
<p>Devices will <b>not</b> yet be able to decrypt history from before they joined the room.</p>
|
||||
<p>Encrypted messages will not be visible on clients that do not yet implement encryption.</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_user_has_used_encryption', true);
|
||||
}
|
||||
},
|
||||
|
||||
_calculatePeekRules: function(room) {
|
||||
var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
|
||||
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
|
||||
|
@ -716,17 +752,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onResendAllClick: function() {
|
||||
var eventsToResend = this._getUnsentMessages(this.state.room);
|
||||
eventsToResend.forEach(function(event) {
|
||||
Resend.resend(event);
|
||||
});
|
||||
Resend.resendUnsentEvents(this.state.room);
|
||||
},
|
||||
|
||||
onCancelAllClick: function() {
|
||||
var eventsToResend = this._getUnsentMessages(this.state.room);
|
||||
eventsToResend.forEach(function(event) {
|
||||
Resend.removeFromQueue(event);
|
||||
});
|
||||
Resend.cancelUnsentEvents(this.state.room);
|
||||
},
|
||||
|
||||
onJoinButtonClicked: function(ev) {
|
||||
|
@ -892,8 +922,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
uploadFile: function(file) {
|
||||
var self = this;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
|
@ -905,11 +933,20 @@ module.exports = React.createClass({
|
|||
|
||||
ContentMessages.sendContentToRoom(
|
||||
file, this.state.room.roomId, MatrixClientPeg.get()
|
||||
).done(undefined, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
).done(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: error,
|
||||
room: this.state.room,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to upload file " + file + " " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to upload file",
|
||||
description: error.toString()
|
||||
description: "Server may be unavailable, overloaded, or the file too big",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -993,9 +1030,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Search failed: " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Search failed",
|
||||
description: error.toString()
|
||||
description: "Server may be unavailable, overloaded, or search timed out :("
|
||||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
|
@ -1639,14 +1677,14 @@ module.exports = React.createClass({
|
|||
|
||||
videoMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
||||
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
|
||||
<TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
|
||||
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
|
||||
width="31" height="27"/>
|
||||
</div>;
|
||||
}
|
||||
voiceMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
|
||||
<TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
|
||||
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
|
||||
width="21" height="26"/>
|
||||
</div>;
|
||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
|||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 3000;
|
||||
const UNPAGINATION_PADDING = 6000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
@ -333,33 +333,27 @@ module.exports = React.createClass({
|
|||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
var itemlist = this.refs.itemlist;
|
||||
var tiles = itemlist.children;
|
||||
const tiles = this.refs.itemlist.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
||||
// Subtract clientHeights to simulate the events being unpaginated whilst counting
|
||||
// the events to be unpaginated.
|
||||
if (backwards) {
|
||||
// Iterate forwards from start of tiles, subtracting event tile height
|
||||
let i = 0;
|
||||
while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i++;
|
||||
// Subtract heights of tiles to simulate the tiles being unpaginated until the
|
||||
// excess height is less than the height of the next tile to subtract. This
|
||||
// prevents excessHeight becoming negative, which could lead to future
|
||||
// pagination.
|
||||
//
|
||||
// If backwards is true, we unpaginate (remove) tiles from the back (top).
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||
// Subtract height of tile as if it were unpaginated
|
||||
excessHeight -= tile.clientHeight;
|
||||
// The tile may not have a scroll token, so guard it
|
||||
if (tile.dataset.scrollToken) {
|
||||
markerScrollToken = tile.dataset.scrollToken;
|
||||
}
|
||||
} else {
|
||||
// Iterate backwards from end of tiles, subtracting event tile height
|
||||
let i = tiles.length - 1;
|
||||
while (i > 0 && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i--;
|
||||
if (tile.clientHeight > excessHeight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -589,24 +583,34 @@ module.exports = React.createClass({
|
|||
var itemlist = this.refs.itemlist;
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var messages = itemlist.children;
|
||||
let newScrollState = null;
|
||||
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var node = messages[i];
|
||||
if (!node.dataset.scrollToken) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
return;
|
||||
newScrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
// If the bottom of the panel intersects the ClientRect of node, use this node
|
||||
// as the scrollToken.
|
||||
// If this is false for the entire for-loop, we default to the last node
|
||||
// (which is why newScrollState is set on every iteration).
|
||||
if (boundingRect.top < wrapperRect.bottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
// This is only false if there were no nodes with `node.dataset.scrollToken` set.
|
||||
if (newScrollState) {
|
||||
this.scrollState = newScrollState;
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
} else {
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
}
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
|
|
|
@ -251,10 +251,12 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
||||
// this particular event should be the first or last to be unpaginated.
|
||||
let eventId = scrollToken;
|
||||
|
||||
let marker = this.state.events.findIndex(
|
||||
|
@ -431,6 +433,10 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
||||
},
|
||||
|
||||
onRoomRedaction: function(ev, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
|
@ -469,14 +475,6 @@ var TimelinePanel = React.createClass({
|
|||
// we still have a client.
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
// if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
|
||||
// to avoid having to wait from the remote echo from the homeserver.
|
||||
if (this.isAtEndOfLiveTimeline()) {
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||
// XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up
|
||||
}
|
||||
|
||||
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||
|
||||
|
@ -514,6 +512,19 @@ var TimelinePanel = React.createClass({
|
|||
// it failed, so allow retries next time the user is active
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
});
|
||||
|
||||
// do a quick-reset of our unreadNotificationCount to avoid having
|
||||
// to wait from the remote echo from the homeserver.
|
||||
// we only do this if we're right at the end, because we're just assuming
|
||||
// that sending an RR for the latest message will set our notif counter
|
||||
// to zero: it may not do this if we send an RR for somewhere before the end.
|
||||
if (this.isAtEndOfLiveTimeline()) {
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||
dis.dispatch({
|
||||
action: 'on_room_read',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -810,7 +821,7 @@ var TimelinePanel = React.createClass({
|
|||
// go via the dispatcher so that the URL is updated
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.timelineSet.roomId,
|
||||
room_id: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,12 +25,13 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
dis.register(this.onAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.mounted = false;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -39,6 +40,10 @@ const REACT_SDK_VERSION =
|
|||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
// 'label' is how we describe it in the UI.
|
||||
const SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
|
@ -135,6 +140,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._addThreepid = null;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
q().then(() => {
|
||||
|
@ -202,9 +208,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to load user settings: " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Can't load user settings",
|
||||
description: error.toString()
|
||||
description: "Server may be unavailable or overloaded",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -242,10 +249,11 @@ module.exports = React.createClass({
|
|||
self._refreshFromServer();
|
||||
}, function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
console.error("Failed to set avatar: " + err);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
description: "Failed to set avatar."
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -256,12 +264,18 @@ module.exports = React.createClass({
|
|||
title: "Sign out?",
|
||||
description:
|
||||
<div>
|
||||
For security, logging out will delete any end-to-end encryption keys from this browser,
|
||||
making previous encrypted chat history unreadable if you log back in.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
|
||||
but for now be warned.
|
||||
For security, logging out will delete any end-to-end encryption keys from this browser.
|
||||
|
||||
If you want to be able to decrypt your conversation history from future Riot sessions,
|
||||
please export your room keys for safe-keeping.
|
||||
</div>,
|
||||
button: "Sign out",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
|
@ -282,6 +296,7 @@ module.exports = React.createClass({
|
|||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||
}
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change password: " + errMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: errMsg
|
||||
|
@ -308,12 +323,16 @@ module.exports = React.createClass({
|
|||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
},
|
||||
|
||||
onAddThreepidClicked: function(value, shouldSubmit) {
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
this._addEmail();
|
||||
},
|
||||
|
||||
_addEmail: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
var email_address = this.refs.add_threepid_input.value;
|
||||
var email_address = this.refs.add_email_input.value;
|
||||
if (!Email.looksValid(email_address)) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invalid Email Address",
|
||||
|
@ -321,10 +340,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
return;
|
||||
}
|
||||
this.add_threepid = new AddThreepid();
|
||||
this._addThreepid = new AddThreepid();
|
||||
// we always bind emails when registering, so let's do the
|
||||
// same here.
|
||||
this.add_threepid.addEmailAddress(email_address, true).done(() => {
|
||||
this._addThreepid.addEmailAddress(email_address, true).done(() => {
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Verification Pending",
|
||||
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||
|
@ -333,12 +352,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
}, (err) => {
|
||||
this.setState({email_add_pending: false});
|
||||
console.error("Unable to add email address " + email_address + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to add email address",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Unable to add email address"
|
||||
});
|
||||
});
|
||||
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
||||
ReactDOM.findDOMNode(this.refs.add_email_input).blur();
|
||||
this.setState({email_add_pending: true});
|
||||
},
|
||||
|
||||
|
@ -357,9 +377,10 @@ module.exports = React.createClass({
|
|||
return this._refreshFromServer();
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to remove contact information",
|
||||
description: err.toString(),
|
||||
title: "Error",
|
||||
description: "Unable to remove contact information",
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
|
@ -376,8 +397,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
verifyEmailAddress: function() {
|
||||
this.add_threepid.checkEmailLinkClicked().done(() => {
|
||||
this.add_threepid = undefined;
|
||||
this._addThreepid.checkEmailLinkClicked().done(() => {
|
||||
this._addThreepid = null;
|
||||
this.setState({
|
||||
phase: "UserSettings.LOADING",
|
||||
});
|
||||
|
@ -397,9 +418,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
} else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify email address: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to verify email address",
|
||||
description: err.toString(),
|
||||
title: "Error",
|
||||
description: "Unable to verify email address",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -419,10 +441,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onClearCacheClicked: function() {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().store.deleteAllData().done(() => {
|
||||
// forceReload=false since we don't really need new HTML/JS files
|
||||
// we just need to restart the JS runtime.
|
||||
window.location.reload(false);
|
||||
PlatformPeg.get().reload();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -745,6 +768,14 @@ module.exports = React.createClass({
|
|||
return medium[0].toUpperCase() + medium.slice(1);
|
||||
},
|
||||
|
||||
presentableTextForThreepid: function(threepid) {
|
||||
if (threepid.medium == 'msisdn') {
|
||||
return '+' + threepid.address;
|
||||
} else {
|
||||
return threepid.address;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
switch (this.state.phase) {
|
||||
|
@ -777,7 +808,9 @@ module.exports = React.createClass({
|
|||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input type="text" key={val.address} id={id} value={val.address} disabled />
|
||||
<input type="text" key={val.address} id={id}
|
||||
value={this.presentableTextForThreepid(val)} disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||
|
@ -785,30 +818,35 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
});
|
||||
var addThreepidSection;
|
||||
let addEmailSection;
|
||||
if (this.state.email_add_pending) {
|
||||
addThreepidSection = <Loader />;
|
||||
addEmailSection = <Loader key="_email_add_spinner" />;
|
||||
} else if (!MatrixClientPeg.get().isGuest()) {
|
||||
addThreepidSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="new">
|
||||
addEmailSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<EditableText
|
||||
ref="add_threepid_input"
|
||||
ref="add_email_input"
|
||||
className="mx_UserSettings_editable"
|
||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||
placeholder={ "Add email address" }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onAddThreepidClicked } />
|
||||
onValueChanged={ this._onAddEmailEditFinished } />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={this._addEmail} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
threepidsSection.push(addThreepidSection);
|
||||
const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber');
|
||||
const addMsisdnSection = (
|
||||
<AddPhoneNumber key="_addMsisdn" onThreepidAdded={this._refreshFromServer} />
|
||||
);
|
||||
threepidsSection.push(addEmailSection);
|
||||
threepidsSection.push(addMsisdnSection);
|
||||
|
||||
var accountJsx;
|
||||
|
||||
|
|
|
@ -93,11 +93,17 @@ module.exports = React.createClass({
|
|||
description:
|
||||
<div>
|
||||
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
||||
but for now be warned.
|
||||
making encrypted chat history unreadable, unless you first export your room keys
|
||||
and re-import them afterwards.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
|
@ -110,6 +116,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
this.setState({
|
||||
[stateKey]: ev.target.value
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,13 +20,13 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../../index');
|
||||
var Signup = require("../../../Signup");
|
||||
var Login = require("../../../Login");
|
||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||
var CasLogin = require("../../views/login/CasLogin");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Signup logic
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Login',
|
||||
|
@ -52,20 +53,20 @@ module.exports = React.createClass({
|
|||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
|
||||
initialErrorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
errorText: this.props.initialErrorText,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
|
||||
// used for preserving username when changing homeserver
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -73,20 +74,21 @@ module.exports = React.createClass({
|
|||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, password) {
|
||||
var self = this;
|
||||
self.setState({
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
||||
self.props.onLoggedIn(data);
|
||||
}, function(error) {
|
||||
self._setStateFromError(error, true);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
this._loginLogic.loginViaPassword(
|
||||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
this._setStateFromError(error, true);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
busy: false
|
||||
});
|
||||
}).done();
|
||||
|
@ -119,6 +121,14 @@ module.exports = React.createClass({
|
|||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onPhoneCountryChanged: function(phoneCountry) {
|
||||
this.setState({ phoneCountry: phoneCountry });
|
||||
},
|
||||
|
||||
onPhoneNumberChanged: function(phoneNumber) {
|
||||
this.setState({ phoneNumber: phoneNumber });
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
|
@ -146,7 +156,7 @@ module.exports = React.createClass({
|
|||
|
||||
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
this._loginLogic = loginLogic;
|
||||
|
@ -225,7 +235,11 @@ module.exports = React.createClass({
|
|||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,25 +15,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
var React = require('react');
|
||||
import q from 'q';
|
||||
import React from 'react';
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
var Signup = require("../../../Signup");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||
var RtsClient = require("../../../RtsClient");
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import CaptchaForm from '../../views/login/CaptchaForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
|
||||
var MIN_PASSWORD_LENGTH = 6;
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* TODO: It would be nice to make use of the InteractiveAuthEntryComponents
|
||||
* here, rather than inventing our own.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Registration',
|
||||
|
||||
|
@ -40,7 +37,7 @@ module.exports = React.createClass({
|
|||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
clientSecret: React.PropTypes.string,
|
||||
sessionId: React.PropTypes.string,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
idSid: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
|
@ -58,7 +55,6 @@ module.exports = React.createClass({
|
|||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
onTeamMemberRegistered: React.PropTypes.func.isRequired,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
|
||||
|
@ -82,27 +78,20 @@ module.exports = React.createClass({
|
|||
formVals: {
|
||||
email: this.props.email,
|
||||
},
|
||||
// true if we're waiting for the user to complete
|
||||
// user-interactive auth
|
||||
// If we've been given a session ID, we're resuming
|
||||
// straight back into UI auth
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// attach this to the instance rather than this.state since it isn't UI
|
||||
this.registerLogic = new Signup.Register(
|
||||
this.props.customHsUrl, this.props.customIsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
}
|
||||
);
|
||||
this.registerLogic.setClientSecret(this.props.clientSecret);
|
||||
this.registerLogic.setSessionId(this.props.sessionId);
|
||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||
if (this.props.referrer) {
|
||||
this.registerLogic.setReferrer(this.props.referrer);
|
||||
}
|
||||
this.registerLogic.recheckState();
|
||||
|
||||
this._replaceClient();
|
||||
|
||||
if (
|
||||
this.props.teamServerConfig &&
|
||||
|
@ -134,154 +123,137 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// may have already done an HTTP hit (e.g. redirect from an email) so
|
||||
// check for any pending response
|
||||
var promise = this.registerLogic.getPromise();
|
||||
if (promise) {
|
||||
this.onProcessingRegistration(promise);
|
||||
}
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.registerLogic.setHomeserverUrl(newHsUrl);
|
||||
this.setState({
|
||||
hsUrl: newHsUrl,
|
||||
});
|
||||
this._replaceClient();
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.registerLogic.setIdentityServerUrl(newIsUrl);
|
||||
this.setState({
|
||||
isUrl: newIsUrl,
|
||||
});
|
||||
this._replaceClient();
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "registration_step_update") {
|
||||
return;
|
||||
}
|
||||
// If the registration state has changed, this means the
|
||||
// user now needs to do something. It would be better
|
||||
// to expose the explicitly in the register logic.
|
||||
this.setState({
|
||||
busy: false
|
||||
_replaceClient: function() {
|
||||
this._matrixClient = Matrix.createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
idBaseUrl: this.state.isUrl,
|
||||
});
|
||||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true,
|
||||
formVals: formVals,
|
||||
doingUIAuth: true,
|
||||
});
|
||||
|
||||
if (formVals.username !== this.props.username) {
|
||||
// don't try to upgrade if we changed our username
|
||||
this.registerLogic.setGuestAccessToken(null);
|
||||
}
|
||||
|
||||
this.onProcessingRegistration(this.registerLogic.register(formVals));
|
||||
},
|
||||
|
||||
// Promise is resolved when the registration process is FULLY COMPLETE
|
||||
onProcessingRegistration: function(promise) {
|
||||
var self = this;
|
||||
promise.done(function(response) {
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
if (!response || !response.access_token) {
|
||||
console.warn(
|
||||
"FIXME: Register fulfilled without a final response, " +
|
||||
"did you break the promise chain?"
|
||||
);
|
||||
// no matter, we'll grab it direct
|
||||
response = self.registerLogic.getCredentials();
|
||||
_onUIAuthFinished: function(success, response, extra) {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdn_available = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
}
|
||||
if (!msisdn_available) {
|
||||
msg = "This server does not support authentication with a phone number";
|
||||
}
|
||||
}
|
||||
if (!response || !response.user_id || !response.access_token) {
|
||||
console.error("Final response is missing keys.");
|
||||
self.setState({
|
||||
errorText: "Registration failed on server"
|
||||
});
|
||||
return;
|
||||
}
|
||||
self.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: self.registerLogic.getHomeserverUrl(),
|
||||
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
this.setState({
|
||||
busy: false,
|
||||
doingUIAuth: false,
|
||||
errorText: msg,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Done regardless of `teamSelected`. People registering with non-team emails
|
||||
// will just nop. The point of this being we might not have the email address
|
||||
// that the user registered with at this stage (depending on whether this
|
||||
// is the client they initiated registration).
|
||||
if (self._rtsClient) {
|
||||
// Track referral if self.props.referrer set, get team_token in order to
|
||||
// retrieve team config and see welcome page etc.
|
||||
self._rtsClient.trackReferral(
|
||||
self.props.referrer || '', // Default to empty string = not referred
|
||||
self.registerLogic.params.idSid,
|
||||
self.registerLogic.params.clientSecret
|
||||
).then((data) => {
|
||||
const teamToken = data.team_token;
|
||||
// Store for use /w welcome pages
|
||||
window.localStorage.setItem('mx_team_token', teamToken);
|
||||
self.props.onTeamMemberRegistered(teamToken);
|
||||
this.setState({
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
busy: true,
|
||||
doingUIAuth: false,
|
||||
});
|
||||
|
||||
self._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
`User successfully registered with team ${team.name}`
|
||||
);
|
||||
if (!team.rooms) {
|
||||
return;
|
||||
// Done regardless of `teamSelected`. People registering with non-team emails
|
||||
// will just nop. The point of this being we might not have the email address
|
||||
// that the user registered with at this stage (depending on whether this
|
||||
// is the client they initiated registration).
|
||||
let trackPromise = q(null);
|
||||
if (this._rtsClient && extra.emailSid) {
|
||||
// Track referral if this.props.referrer set, get team_token in order to
|
||||
// retrieve team config and see welcome page etc.
|
||||
trackPromise = this._rtsClient.trackReferral(
|
||||
this.props.referrer || '', // Default to empty string = not referred
|
||||
extra.emailSid,
|
||||
extra.clientSecret,
|
||||
).then((data) => {
|
||||
const teamToken = data.team_token;
|
||||
// Store for use /w welcome pages
|
||||
window.localStorage.setItem('mx_team_token', teamToken);
|
||||
|
||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
`User successfully registered with team ${team.name}`
|
||||
);
|
||||
if (!team.rooms) {
|
||||
return;
|
||||
}
|
||||
// Auto-join rooms
|
||||
team.rooms.forEach((room) => {
|
||||
if (room.auto_join && room.room_id) {
|
||||
console.log(`Auto-joining ${room.room_id}`);
|
||||
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||
}
|
||||
// Auto-join rooms
|
||||
team.rooms.forEach((room) => {
|
||||
if (room.auto_join && room.room_id) {
|
||||
console.log(`Auto-joining ${room.room_id}`);
|
||||
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||
}
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error getting team config', err);
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error tracking referral', err);
|
||||
console.error('Error getting team config', err);
|
||||
});
|
||||
}
|
||||
|
||||
if (self.props.brand) {
|
||||
MatrixClientPeg.get().getPushers().done((resp)=>{
|
||||
var pushers = resp.pushers;
|
||||
for (var i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
var emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: self.props.brand };
|
||||
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
|
||||
console.log("Set email branding to " + self.props.brand);
|
||||
}, (error) => {
|
||||
console.error("Couldn't set email branding: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
console.error("Couldn't get pushers: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
}, function(err) {
|
||||
if (err.message) {
|
||||
self.setState({
|
||||
errorText: err.message
|
||||
});
|
||||
}
|
||||
self.setState({
|
||||
busy: false
|
||||
return teamToken;
|
||||
}, (err) => {
|
||||
console.error('Error tracking referral', err);
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
trackPromise.then((teamToken) => {
|
||||
console.info('Team token promise',teamToken);
|
||||
this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
}, teamToken);
|
||||
}).then(() => {
|
||||
return this._setupPushers();
|
||||
});
|
||||
},
|
||||
|
||||
_setupPushers: function() {
|
||||
if (!this.props.brand) {
|
||||
return q();
|
||||
}
|
||||
return MatrixClientPeg.get().getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
|
||||
console.log("Set email branding to " + this.props.brand);
|
||||
}, (error) => {
|
||||
console.error("Couldn't set email branding: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
console.error("Couldn't get pushers: " + error);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -300,6 +272,9 @@ module.exports = React.createClass({
|
|||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = "This doesn't look like a valid email address";
|
||||
break;
|
||||
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||
errMsg = "This doesn't look like a valid phone number";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||
break;
|
||||
|
@ -316,116 +291,121 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onCaptchaResponse: function(response) {
|
||||
this.registerLogic.tellStage("m.login.recaptcha", {
|
||||
response: response
|
||||
});
|
||||
},
|
||||
|
||||
onTeamSelected: function(teamSelected) {
|
||||
if (!this._unmounted) {
|
||||
this.setState({ teamSelected });
|
||||
}
|
||||
},
|
||||
|
||||
_getRegisterContentJsx: function() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
_makeRegisterRequest: function(auth) {
|
||||
let guestAccessToken = this.props.guestAccessToken;
|
||||
|
||||
var currStep = this.registerLogic.getStep();
|
||||
var registerStep;
|
||||
switch (currStep) {
|
||||
case "Register.COMPLETE":
|
||||
break; // NOP
|
||||
case "Register.START":
|
||||
case "Register.STEP_m.login.dummy":
|
||||
// NB. Our 'username' prop is specifically for upgrading
|
||||
// a guest account
|
||||
if (this.state.teamServerBusy) {
|
||||
registerStep = <Spinner />;
|
||||
break;
|
||||
}
|
||||
registerStep = (
|
||||
if (
|
||||
this.state.formVals.username !== this.props.username ||
|
||||
this.state.hsUrl != this.props.defaultHsUrl
|
||||
) {
|
||||
// don't try to upgrade if we changed our username
|
||||
// or are registering on a different HS
|
||||
guestAccessToken = null;
|
||||
}
|
||||
|
||||
// Only send the bind params if we're sending username / pw params
|
||||
// (Since we need to send no params at all to use the ones saved in the
|
||||
// session).
|
||||
const bindThreepids = this.state.formVals.password ? {
|
||||
email: true,
|
||||
msisdn: true,
|
||||
} : {};
|
||||
|
||||
return this._matrixClient.register(
|
||||
this.state.formVals.username,
|
||||
this.state.formVals.password,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
bindThreepids,
|
||||
guestAccessToken,
|
||||
);
|
||||
},
|
||||
|
||||
_getUIAuthInputs: function() {
|
||||
return {
|
||||
emailAddress: this.state.formVals.email,
|
||||
phoneCountry: this.state.formVals.phoneCountry,
|
||||
phoneNumber: this.state.formVals.phoneNumber,
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
||||
|
||||
let registerBody;
|
||||
if (this.state.doingUIAuth) {
|
||||
registerBody = (
|
||||
<InteractiveAuth
|
||||
matrixClient={this._matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
poll={true}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let guestUsername = this.props.username;
|
||||
if (this.state.hsUrl != this.props.defaultHsUrl) {
|
||||
guestUsername = null;
|
||||
}
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
|
||||
}
|
||||
registerBody = (
|
||||
<div>
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
teamsConfig={this.state.teamsConfig}
|
||||
guestUsername={this.props.username}
|
||||
guestUsername={guestUsername}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.email.identity":
|
||||
registerStep = (
|
||||
<div>
|
||||
Please check your email to continue registration.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.recaptcha":
|
||||
var publicKey;
|
||||
var serverParams = this.registerLogic.getServerData().params;
|
||||
if (serverParams && serverParams["m.login.recaptcha"]) {
|
||||
publicKey = serverParams["m.login.recaptcha"].public_key;
|
||||
}
|
||||
|
||||
registerStep = (
|
||||
<CaptchaForm sitePublicKey={publicKey}
|
||||
onCaptchaResponse={this.onCaptchaResponse}
|
||||
{errorSection}
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown register state: %s", currStep);
|
||||
break;
|
||||
}
|
||||
var busySpinner;
|
||||
if (this.state.busy) {
|
||||
busySpinner = (
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var returnToAppJsx;
|
||||
let returnToAppJsx;
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
Return to app
|
||||
</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Create an account</h2>
|
||||
{registerStep}
|
||||
<div className="mx_Login_error">{this.state.errorText}</div>
|
||||
{busySpinner}
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={ this.registerLogic.getStep() === "Register.START" }
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={1000} />
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
I already have an account
|
||||
</a>
|
||||
{ returnToAppJsx }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
var LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
|
@ -435,7 +415,12 @@ module.exports = React.createClass({
|
|||
this.state.teamSelected.domain + "/icon.png" :
|
||||
null}
|
||||
/>
|
||||
{this._getRegisterContentJsx()}
|
||||
<h2>Create an account</h2>
|
||||
{registerBody}
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
I already have an account
|
||||
</a>
|
||||
{returnToAppJsx}
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -65,15 +66,14 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<img
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
alt="Cancel" title="Cancel"
|
||||
/>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
{ this.props.title }
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Unread from '../../../Unread';
|
||||
import classNames from 'classnames';
|
||||
import createRoom from '../../../createRoom';
|
||||
|
||||
export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onNewDMClick = this.onNewDMClick.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
}
|
||||
|
||||
onNewDMClick() {
|
||||
createRoom({dmUserId: this.props.userId});
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.userId);
|
||||
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelClasses = classNames({
|
||||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.onNewDMClick}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src="img/create-big.svg" width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>Start new chat</i></div>
|
||||
</AccessibleButton>;
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={() => {
|
||||
this.props.onFinished(false)
|
||||
}}
|
||||
title='Create a new chat or reuse an existing one'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
You already have existing direct chats with this user:
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{tiles}
|
||||
{startNewChat}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
|
@ -26,18 +26,10 @@ import dis from '../../../dispatcher';
|
|||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
|
||||
/*
|
||||
* Escapes a string so it can be used in a RegExp
|
||||
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
|
||||
* From http://stackoverflow.com/a/6969486
|
||||
*/
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "ChatInviteDialog",
|
||||
propTypes: {
|
||||
|
@ -85,6 +77,19 @@ module.exports = React.createClass({
|
|||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
}
|
||||
// Create a Fuse instance for fuzzy searching this._userList
|
||||
this._fuse = new Fuse(
|
||||
// Use an empty list at first that will later be populated
|
||||
// (see this._updateUserList)
|
||||
[],
|
||||
{
|
||||
shouldSort: true,
|
||||
location: 0, // The index of the query in the test string
|
||||
distance: 5, // The distance away from location the query can be
|
||||
// 0.0 = exact match, 1.0 = match anything
|
||||
threshold: 0.3,
|
||||
}
|
||||
);
|
||||
this._updateUserList();
|
||||
},
|
||||
|
||||
|
@ -97,18 +102,27 @@ module.exports = React.createClass({
|
|||
if (inviteList === null) return;
|
||||
}
|
||||
|
||||
const addrTexts = inviteList.map(addr => addr.address);
|
||||
if (inviteList.length > 0) {
|
||||
if (this._isDmChat(inviteList)) {
|
||||
if (this._isDmChat(addrTexts)) {
|
||||
const userId = inviteList[0].address;
|
||||
// Direct Message chat
|
||||
var room = this._getDirectMessageRoom(inviteList[0]);
|
||||
if (room) {
|
||||
// A Direct Message room already exists for this user and you
|
||||
// so go straight to that room
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
const rooms = this._getDirectMessageRooms(userId);
|
||||
if (rooms.length > 0) {
|
||||
// A Direct Message room already exists for this user, so select a
|
||||
// room from a list that is similar to the one in MemberInfo panel
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
"views.dialogs.ChatCreateOrReuseDialog"
|
||||
);
|
||||
Modal.createDialog(ChatCreateOrReuseDialog, {
|
||||
userId: userId,
|
||||
onFinished: (success) => {
|
||||
if (success) {
|
||||
this.props.onFinished(true, inviteList[0]);
|
||||
}
|
||||
// else show this ChatInviteDialog again
|
||||
}
|
||||
});
|
||||
this.props.onFinished(true, inviteList[0]);
|
||||
} else {
|
||||
this._startChat(inviteList);
|
||||
}
|
||||
|
@ -167,45 +181,59 @@ module.exports = React.createClass({
|
|||
const query = ev.target.value;
|
||||
let queryList = [];
|
||||
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@') {
|
||||
// filter the known users list
|
||||
queryList = this._userList.filter((user) => {
|
||||
return this._matches(query, user);
|
||||
}).map((user) => {
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
return {
|
||||
addressType: 'mx',
|
||||
address: user.userId,
|
||||
displayName: user.displayName,
|
||||
avatarMxc: user.avatarUrl,
|
||||
isKnown: true,
|
||||
}
|
||||
});
|
||||
if (query.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the query isn't a user we know about, but is a
|
||||
// valid address, add an entry for that
|
||||
if (queryList.length == 0) {
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@') {
|
||||
// Weighted keys prefer to match userIds when first char is @
|
||||
this._fuse.options.keys = [{
|
||||
name: 'displayName',
|
||||
weight: query[0] === '@' ? 0.1 : 0.9,
|
||||
},{
|
||||
name: 'userId',
|
||||
weight: query[0] === '@' ? 0.9 : 0.1,
|
||||
}];
|
||||
queryList = this._fuse.search(query).map((user) => {
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
return {
|
||||
addressType: 'mx',
|
||||
address: user.userId,
|
||||
displayName: user.displayName,
|
||||
avatarMxc: user.avatarUrl,
|
||||
isKnown: true,
|
||||
}
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList[0] = {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
};
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
queryList: queryList,
|
||||
error: false,
|
||||
});
|
||||
this.setState({
|
||||
queryList: queryList,
|
||||
error: false,
|
||||
}, () => {
|
||||
this.addressSelector.moveSelectionTop();
|
||||
});
|
||||
}, 200);
|
||||
},
|
||||
|
||||
onDismissed: function(index) {
|
||||
|
@ -238,22 +266,20 @@ module.exports = React.createClass({
|
|||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_getDirectMessageRoom: function(addr) {
|
||||
_getDirectMessageRooms: function(addr) {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
var dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
if (dmRooms.length > 0) {
|
||||
// Cycle through all the DM rooms and find the first non forgotten or parted room
|
||||
for (let i = 0; i < dmRooms.length; i++) {
|
||||
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
|
||||
if (room) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me.membership == 'join') {
|
||||
return room;
|
||||
}
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
const rooms = [];
|
||||
dmRooms.forEach(dmRoom => {
|
||||
let room = MatrixClientPeg.get().getRoom(dmRoom);
|
||||
if (room) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me.membership == 'join') {
|
||||
rooms.push(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return rooms;
|
||||
},
|
||||
|
||||
_startChat: function(addrs) {
|
||||
|
@ -282,8 +308,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite",
|
||||
description: err.toString()
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -295,8 +321,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite user",
|
||||
description: err.toString()
|
||||
title: "Error",
|
||||
description: "Failed to invite user",
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -316,8 +342,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite",
|
||||
description: err.toString()
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -331,49 +357,15 @@ module.exports = React.createClass({
|
|||
_updateUserList: new rate_limited_func(function() {
|
||||
// Get all the users
|
||||
this._userList = MatrixClientPeg.get().getUsers();
|
||||
// Remove current user
|
||||
const meIx = this._userList.findIndex((u) => {
|
||||
return u.userId === MatrixClientPeg.get().credentials.userId;
|
||||
});
|
||||
this._userList.splice(meIx, 1);
|
||||
|
||||
this._fuse.set(this._userList);
|
||||
}, 500),
|
||||
|
||||
// This is the search algorithm for matching users
|
||||
_matches: function(query, user) {
|
||||
var name = user.displayName.toLowerCase();
|
||||
var uid = user.userId.toLowerCase();
|
||||
query = query.toLowerCase();
|
||||
|
||||
// don't match any that are already on the invite list
|
||||
if (this._isOnInviteList(uid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore current user
|
||||
if (uid === MatrixClientPeg.get().credentials.userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// direct prefix matches
|
||||
if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// strip @ on uid and try matching again
|
||||
if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find the query following a "word boundary", except that
|
||||
// this does avoids using \b because it only considers letters from
|
||||
// the roman alphabet to be word characters.
|
||||
// Instead, we look for the query following either:
|
||||
// * The start of the string
|
||||
// * Whitespace, or
|
||||
// * A fixed number of punctuation characters
|
||||
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
||||
if (expr.test(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_isOnInviteList: function(uid) {
|
||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||
if (
|
||||
|
@ -386,8 +378,11 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
_isDmChat: function(addrs) {
|
||||
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
_isDmChat: function(addrTexts) {
|
||||
if (addrTexts.length === 1 &&
|
||||
getAddressType(addrTexts[0]) === "mx" &&
|
||||
!this.props.roomId
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
danger: false,
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = "Confirm Redaction";
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': false,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
Are you sure you wish to redact (delete) this event?
|
||||
Note that if you redact a room name or topic change, it could undo the change.
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
Redact
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -97,7 +97,7 @@ export default React.createClass({
|
|||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={72} height={72} />
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Lifecycle from '../../../Lifecycle';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Velocity from 'velocity-vector';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
|
|
|
@ -27,6 +27,9 @@ export default React.createClass({
|
|||
displayName: 'InteractiveAuthDialog',
|
||||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
|
@ -49,22 +52,62 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
}
|
||||
},
|
||||
|
||||
_onAuthFinished: function(success, result) {
|
||||
if (success) {
|
||||
this.props.onFinished(true, result);
|
||||
} else {
|
||||
this.setState({
|
||||
authError: result,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onDismissClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let content;
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
<div>
|
||||
<div>{this.state.authError.message || this.state.authError.toString()}</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
>
|
||||
Dismiss
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onAuthFinished={this._onAuthFinished}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
title={this.state.authError ? 'Error' : this.props.title}
|
||||
>
|
||||
<div>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
</div>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -21,10 +21,8 @@ export default React.createClass({
|
|||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
]),
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
@ -34,8 +32,10 @@ export default React.createClass({
|
|||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
extraButtons: null,
|
||||
button: "OK",
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -49,6 +49,11 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
) : null;
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
|
@ -61,10 +66,8 @@ export default React.createClass({
|
|||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -16,8 +16,10 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -85,7 +87,7 @@ UnknownDeviceList.propTypes = {
|
|||
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'UnknownEventDialog',
|
||||
displayName: 'UnknownDeviceDialog',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
@ -125,14 +127,10 @@ export default React.createClass({
|
|||
} else {
|
||||
warning = (
|
||||
<div>
|
||||
<p>
|
||||
This means there is no guarantee that the devices
|
||||
belong to the users they claim to.
|
||||
</p>
|
||||
<p>
|
||||
We recommend you go through the verification process
|
||||
for each device before continuing, but you can resend
|
||||
the message without verifying if you prefer.
|
||||
for each device to confirm they belong to their legitimate owner,
|
||||
but you can resend the message without verifying if you prefer.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -151,8 +149,7 @@ export default React.createClass({
|
|||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
This room contains unknown devices which have not been
|
||||
verified.
|
||||
This room contains devices that you haven't seen before.
|
||||
</h4>
|
||||
{ warning }
|
||||
Unknown devices:
|
||||
|
@ -160,6 +157,13 @@ export default React.createClass({
|
|||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
Send anyway
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
|
|
|
@ -27,8 +27,8 @@ import React from 'react';
|
|||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, ...restProps} = props;
|
||||
restProps.onClick = onClick;
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
||||
restProps.onKeyUp = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
|
|
|
@ -61,6 +61,15 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
moveSelectionTop: function() {
|
||||
if (this.state.selected > 0) {
|
||||
this.setState({
|
||||
selected: 0,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
moveSelectionUp: function() {
|
||||
if (this.state.selected > 0) {
|
||||
this.setState({
|
||||
|
@ -124,7 +133,14 @@ export default React.createClass({
|
|||
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
||||
// method, how far to scroll when using the arrow keys
|
||||
addressList.push(
|
||||
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
||||
<div
|
||||
className={classes}
|
||||
onClick={this.onClick.bind(this, i)}
|
||||
onMouseEnter={this.onMouseEnter.bind(this, i)}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
key={this.props.addressList[i].userId}
|
||||
ref={(ref) => { this.addressListElement = ref; }}
|
||||
>
|
||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -64,19 +64,14 @@ export default React.createClass({
|
|||
const address = this.props.address;
|
||||
const name = address.displayName || address.address;
|
||||
|
||||
let imgUrl;
|
||||
if (address.avatarMxc) {
|
||||
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
);
|
||||
}
|
||||
let imgUrls = [];
|
||||
|
||||
if (address.addressType === "mx") {
|
||||
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
||||
if (address.addressType === "mx" && address.avatarMxc) {
|
||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
));
|
||||
} else if (address.addressType === 'email') {
|
||||
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
||||
} else {
|
||||
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
||||
imgUrls.push('img/icon-email-user.svg');
|
||||
}
|
||||
|
||||
// Removing networks for now as they're not really supported
|
||||
|
@ -168,7 +163,7 @@ export default React.createClass({
|
|||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_AddressTile_avatar">
|
||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||
<BaseAvatar defaultToInitialLetter={true} width={25} height={25} name={name} title={name} urls={imgUrls} />
|
||||
</div>
|
||||
{ info }
|
||||
{ dismiss }
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
}
|
||||
|
||||
_onKeyUp(ev) {
|
||||
if (ev.key == 'Enter') {
|
||||
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||
if (this.props.onJoinClick) {
|
||||
this.props.onJoinClick(this.state.value);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
class MenuOption extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.props.onMouseEnter(this.props.dropdownKey);
|
||||
}
|
||||
|
||||
_onClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onClick(this.props.dropdownKey);
|
||||
}
|
||||
|
||||
render() {
|
||||
const optClasses = classnames({
|
||||
mx_Dropdown_option: true,
|
||||
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||
});
|
||||
|
||||
return <div className={optClasses}
|
||||
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
React.PropTypes.node
|
||||
]),
|
||||
highlighted: React.PropTypes.bool,
|
||||
dropdownKey: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
onMouseEnter: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/*
|
||||
* Reusable dropdown select control, akin to react-select,
|
||||
* but somewhat simpler as react-select is 79KB of minified
|
||||
* javascript.
|
||||
*
|
||||
* TODO: Port NetworkDropdown to use this.
|
||||
*/
|
||||
export default class Dropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.dropdownRootElement = null;
|
||||
this.ignoreEvent = null;
|
||||
|
||||
this._onInputClick = this._onInputClick.bind(this);
|
||||
this._onRootClick = this._onRootClick.bind(this);
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||
this._onInputKeyPress = this._onInputKeyPress.bind(this);
|
||||
this._onInputKeyUp = this._onInputKeyUp.bind(this);
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._collectRoot = this._collectRoot.bind(this);
|
||||
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||
this._setHighlightedOption = this._setHighlightedOption.bind(this);
|
||||
|
||||
this.inputTextBox = null;
|
||||
|
||||
this._reindexChildren(this.props.children);
|
||||
|
||||
const firstChild = React.Children.toArray(props.children)[0];
|
||||
|
||||
this.state = {
|
||||
// True if the menu is dropped-down
|
||||
expanded: false,
|
||||
// The key of the highlighted option
|
||||
// (the option that would become selected if you pressed enter)
|
||||
highlightedOption: firstChild ? firstChild.key : null,
|
||||
// the current search query
|
||||
searchQuery: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this._onDocumentClick, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this._onDocumentClick, false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._reindexChildren(nextProps.children);
|
||||
const firstChild = React.Children.toArray(nextProps.children)[0];
|
||||
this.setState({
|
||||
highlightedOption: firstChild ? firstChild.key : null,
|
||||
});
|
||||
}
|
||||
|
||||
_reindexChildren(children) {
|
||||
this.childrenByKey = {};
|
||||
React.Children.forEach(children, (child) => {
|
||||
this.childrenByKey[child.key] = child;
|
||||
});
|
||||
}
|
||||
|
||||
_onDocumentClick(ev) {
|
||||
// Close the dropdown if the user clicks anywhere that isn't
|
||||
// within our root element
|
||||
if (ev !== this.ignoreEvent) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onRootClick(ev) {
|
||||
// This captures any clicks that happen within our elements,
|
||||
// such that we can then ignore them when they're seen by the
|
||||
// click listener on the document handler, ie. not close the
|
||||
// dropdown immediately after opening it.
|
||||
// NB. We can't just stopPropagation() because then the event
|
||||
// doesn't reach the React onClick().
|
||||
this.ignoreEvent = ev;
|
||||
}
|
||||
|
||||
_onInputClick(ev) {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
_onMenuOptionClick(dropdownKey) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
this.props.onOptionChange(dropdownKey);
|
||||
}
|
||||
|
||||
_onInputKeyPress(e) {
|
||||
// This needs to be on the keypress event because otherwise
|
||||
// it can't cancel the form submission
|
||||
if (e.key == 'Enter') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_onInputKeyUp(e) {
|
||||
// These keys don't generate keypress events and so needs to
|
||||
// be on keyup
|
||||
if (e.key == 'Escape') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
} else if (e.key == 'ArrowDown') {
|
||||
this.setState({
|
||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||
});
|
||||
} else if (e.key == 'ArrowUp') {
|
||||
this.setState({
|
||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onInputChange(e) {
|
||||
this.setState({
|
||||
searchQuery: e.target.value,
|
||||
});
|
||||
if (this.props.onSearchChange) {
|
||||
this.props.onSearchChange(e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
_collectRoot(e) {
|
||||
if (this.dropdownRootElement) {
|
||||
this.dropdownRootElement.removeEventListener(
|
||||
'click', this._onRootClick, false,
|
||||
);
|
||||
}
|
||||
if (e) {
|
||||
e.addEventListener('click', this._onRootClick, false);
|
||||
}
|
||||
this.dropdownRootElement = e;
|
||||
}
|
||||
|
||||
_collectInputTextBox(e) {
|
||||
this.inputTextBox = e;
|
||||
if (e) e.focus();
|
||||
}
|
||||
|
||||
_setHighlightedOption(optionKey) {
|
||||
this.setState({
|
||||
highlightedOption: optionKey,
|
||||
});
|
||||
}
|
||||
|
||||
_nextOption(optionKey) {
|
||||
const keys = Object.keys(this.childrenByKey);
|
||||
const index = keys.indexOf(optionKey);
|
||||
return keys[(index + 1) % keys.length];
|
||||
}
|
||||
|
||||
_prevOption(optionKey) {
|
||||
const keys = Object.keys(this.childrenByKey);
|
||||
const index = keys.indexOf(optionKey);
|
||||
return keys[(index - 1) % keys.length];
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
const options = React.Children.map(this.props.children, (child) => {
|
||||
return (
|
||||
<MenuOption key={child.key} dropdownKey={child.key}
|
||||
highlighted={this.state.highlightedOption == child.key}
|
||||
onMouseEnter={this._setHighlightedOption}
|
||||
onClick={this._onMenuOptionClick}
|
||||
>
|
||||
{child}
|
||||
</MenuOption>
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.state.searchQuery) {
|
||||
options.push(
|
||||
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
|
||||
Type to search...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
render() {
|
||||
let currentValue;
|
||||
|
||||
const menuStyle = {};
|
||||
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{this._getMenuOptions()}
|
||||
</div>;
|
||||
} else {
|
||||
const selectedChild = this.props.getShortOption ?
|
||||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
currentValue = <div className="mx_Dropdown_option">
|
||||
{selectedChild}
|
||||
</div>
|
||||
}
|
||||
|
||||
const dropdownClasses = {
|
||||
mx_Dropdown: true,
|
||||
};
|
||||
if (this.props.className) {
|
||||
dropdownClasses[this.props.className] = true;
|
||||
}
|
||||
|
||||
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||
// to the input, but overflows below it. The root contains both.
|
||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
|
||||
{currentValue}
|
||||
<span className="mx_Dropdown_arrow"></span>
|
||||
{menu}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.propTypes = {
|
||||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth: React.PropTypes.number,
|
||||
// Called when the selected option changes
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
}
|
|
@ -33,7 +33,10 @@ module.exports = React.createClass({
|
|||
className: React.PropTypes.string,
|
||||
labelClassName: React.PropTypes.string,
|
||||
placeholderClassName: React.PropTypes.string,
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel: React.PropTypes.bool,
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
|
@ -51,6 +54,7 @@ module.exports = React.createClass({
|
|||
editable: true,
|
||||
className: "mx_EditableText",
|
||||
placeholderClassName: "mx_EditableText_placeholder",
|
||||
blurToSubmit: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -119,6 +123,7 @@ module.exports = React.createClass({
|
|||
this.value = this.props.initialValue;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.onValueChanged(false);
|
||||
this.refs.editable_div.blur();
|
||||
},
|
||||
|
||||
onValueChanged: function(shouldSubmit) {
|
||||
|
@ -182,13 +187,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onFinish: function(ev) {
|
||||
onFinish: function(ev, shouldSubmit) {
|
||||
var self = this;
|
||||
var submit = (ev.key === "Enter");
|
||||
var submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
self.onValueChanged(submit);
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -199,7 +206,7 @@ module.exports = React.createClass({
|
|||
if (this.props.blurToCancel)
|
||||
{this.cancelEdit();}
|
||||
else
|
||||
{this.onFinish(ev);}
|
||||
{this.onFinish(ev, this.props.blurToSubmit);}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
|
|
@ -116,6 +116,7 @@ export default class EditableTextContainer extends React.Component {
|
|||
<EditableText initialValue={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
onValueChanged={this._onValueChanged}
|
||||
blurToSubmit={this.props.blurToSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -137,11 +138,15 @@ EditableTextContainer.propTypes = {
|
|||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: React.PropTypes.func,
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
EditableTextContainer.defaultProps = {
|
||||
initialValue: "",
|
||||
placeholder: "",
|
||||
blurToSubmit: false,
|
||||
onSubmit: function(v) {return q(); },
|
||||
};
|
||||
|
|
|
@ -30,6 +30,8 @@ module.exports = React.createClass({
|
|||
avatarsMaxLength: React.PropTypes.number,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: React.PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -63,6 +65,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
this.props.onToggle();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -108,7 +111,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summaries.join(", ")}
|
||||
</span>
|
||||
);
|
||||
|
@ -264,7 +267,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return (
|
||||
<span>
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
{avatars}
|
||||
</span>
|
||||
);
|
||||
|
@ -397,31 +400,28 @@ module.exports = React.createClass({
|
|||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||
);
|
||||
|
||||
const avatars = this._renderAvatars(avatarMembers);
|
||||
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
||||
const toggleButton = (
|
||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
</a>
|
||||
);
|
||||
|
||||
const summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
{avatars}
|
||||
</span>
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summary}
|
||||
</span>
|
||||
{toggleButton}
|
||||
let summaryContainer = null;
|
||||
if (!expanded) {
|
||||
summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
{this._renderAvatars(avatarMembers)}
|
||||
{this._renderSummary(aggregate.names, orderedTransitionSequences)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const toggleButton = (
|
||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
{toggleButton}
|
||||
{summaryContainer}
|
||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||
{expandedEvents}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,17 +16,12 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var roles = {
|
||||
0: 'User',
|
||||
50: 'Moderator',
|
||||
100: 'Admin',
|
||||
};
|
||||
import React from 'react';
|
||||
import * as Roles from '../../../Roles';
|
||||
|
||||
var reverseRoles = {};
|
||||
Object.keys(roles).forEach(function(key) {
|
||||
reverseRoles[roles[key]] = key;
|
||||
Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -49,7 +44,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (roles[this.props.value] === undefined),
|
||||
custom: (Roles.LEVEL_ROLE_MAP[this.props.value] === undefined),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -99,22 +94,34 @@ module.exports = React.createClass({
|
|||
selectValue = "Custom";
|
||||
}
|
||||
else {
|
||||
selectValue = roles[this.props.value] || "Custom";
|
||||
selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
}
|
||||
var select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
}
|
||||
else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
return {
|
||||
value: Roles.LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
}
|
||||
});
|
||||
options.push({ value: "Custom", text: "Custom level" });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value}>{op.text}</option>;
|
||||
});
|
||||
|
||||
select =
|
||||
<select ref="select"
|
||||
value={ this.props.controlled ? selectValue : undefined }
|
||||
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
||||
onChange={ this.onSelectChange }>
|
||||
<option value="User">User (0)</option>
|
||||
<option value="Moderator">Moderator (50)</option>
|
||||
<option value="Admin">Admin (100)</option>
|
||||
<option value="Custom">Custom level</option>
|
||||
{ options }
|
||||
</select>;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import { charactersToImageNode } from '../../../HtmlUtils';
|
||||
|
||||
const COUNTRIES_BY_ISO2 = new Object(null);
|
||||
for (const c of COUNTRIES) {
|
||||
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||
}
|
||||
|
||||
function countryMatchesSearchQuery(query, country) {
|
||||
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
if (country.iso2 == query.toUpperCase()) return true;
|
||||
if (country.prefix == query) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const MAX_DISPLAYED_ROWS = 2;
|
||||
|
||||
export default class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
this.props.onOptionChange(COUNTRIES[0].iso2);
|
||||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
// Unicode Regional Indicator Symbol letter 'A'
|
||||
const RIS_A = 0x1F1E6;
|
||||
const ASCII_A = 65;
|
||||
return charactersToImageNode(iso2,
|
||||
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
||||
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let displayedCountries;
|
||||
if (this.state.searchQuery) {
|
||||
displayedCountries = COUNTRIES.filter(
|
||||
countryMatchesSearchQuery.bind(this, this.state.searchQuery),
|
||||
);
|
||||
if (
|
||||
this.state.searchQuery.length == 2 &&
|
||||
COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]
|
||||
) {
|
||||
// exact ISO2 country name match: make the first result the matches ISO2
|
||||
const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()];
|
||||
displayedCountries = displayedCountries.filter((c) => {
|
||||
return c.iso2 != matched.iso2;
|
||||
});
|
||||
displayedCountries.unshift(matched);
|
||||
}
|
||||
} else {
|
||||
displayedCountries = COUNTRIES;
|
||||
}
|
||||
|
||||
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
|
||||
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
|
||||
}
|
||||
|
||||
const options = displayedCountries.map((country) => {
|
||||
return <div key={country.iso2}>
|
||||
{this._flagImgForIso2(country.iso2)}
|
||||
{country.name}
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
const value = this.props.value || COUNTRIES[0].iso2;
|
||||
|
||||
return <Dropdown className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._flagImgForIso2}
|
||||
value={value}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
};
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -28,13 +29,32 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
* Call getEntryComponentForLoginType() to get a component suitable for a
|
||||
* particular login type. Each component requires the same properties:
|
||||
*
|
||||
* matrixClient: A matrix client. May be a different one to the one
|
||||
* currently being used generally (eg. to register with
|
||||
* one HS whilst beign a guest on another).
|
||||
* loginType: the login type of the auth stage being attempted
|
||||
* authSessionId: session id from the server
|
||||
* clientSecret: The client secret in use for ID server auth sessions
|
||||
* stageParams: params from the server for the stage being attempted
|
||||
* errorText: error message from a previous attempt to authenticate
|
||||
* submitAuthDict: a function which will be called with the new auth dict
|
||||
* busy: a boolean indicating whether the auth logic is doing something
|
||||
* the user needs to wait for.
|
||||
* inputs: Object of inputs provided by the user, as in js-sdk
|
||||
* interactive-auth
|
||||
* stageState: Stage-specific object used for communicating state information
|
||||
* to the UI from the state-specific auth logic.
|
||||
* Defined keys for stages are:
|
||||
* m.login.email.identity:
|
||||
* * emailSid: string representing the sid of the active
|
||||
* verification session from the ID server, or
|
||||
* null if no session is active.
|
||||
* fail: a function which should be called with an error object if an
|
||||
* error occurred during the auth stage. This will cause the auth
|
||||
* session to be failed and the process to go back to the start.
|
||||
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||
* email sid after a token is requested.
|
||||
* makeRegistrationUrl A function that makes a registration URL
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
|
@ -48,6 +68,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
// is the auth logic currently waiting for something to
|
||||
|
@ -73,7 +94,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
password: this.refs.passwordField.value,
|
||||
});
|
||||
},
|
||||
|
@ -139,6 +160,7 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
stageParams: React.PropTypes.object.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
busy: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
|
@ -149,6 +171,11 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||
var sitePublicKey = this.props.stageParams.public_key;
|
||||
return (
|
||||
|
@ -164,10 +191,214 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const EmailIdentityAuthEntry = React.createClass({
|
||||
displayName: 'EmailIdentityAuthEntry',
|
||||
|
||||
statics: {
|
||||
LOGIN_TYPE: "m.login.email.identity",
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
authSessionId: React.PropTypes.string.isRequired,
|
||||
clientSecret: React.PropTypes.string.isRequired,
|
||||
inputs: React.PropTypes.object.isRequired,
|
||||
stageState: React.PropTypes.object.isRequired,
|
||||
fail: React.PropTypes.func.isRequired,
|
||||
setEmailSid: React.PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
requestingToken: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (this.props.stageState.emailSid === null) {
|
||||
this.setState({requestingToken: true});
|
||||
this._requestEmailToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Requests a verification token by email.
|
||||
*/
|
||||
_requestEmailToken: function() {
|
||||
const nextLink = this.props.makeRegistrationUrl({
|
||||
client_secret: this.props.clientSecret,
|
||||
hs_url: this.props.matrixClient.getHomeserverUrl(),
|
||||
is_url: this.props.matrixClient.getIdentityServerUrl(),
|
||||
session_id: this.props.authSessionId,
|
||||
});
|
||||
|
||||
return this.props.matrixClient.requestRegisterEmailToken(
|
||||
this.props.inputs.emailAddress,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
nextLink,
|
||||
).then((result) => {
|
||||
this.props.setEmailSid(result.sid);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p>
|
||||
<p>Please check your email to continue registration.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const MsisdnAuthEntry = React.createClass({
|
||||
displayName: 'MsisdnAuthEntry',
|
||||
|
||||
statics: {
|
||||
LOGIN_TYPE: "m.login.msisdn",
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
inputs: React.PropTypes.shape({
|
||||
phoneCountry: React.PropTypes.string,
|
||||
phoneNumber: React.PropTypes.string,
|
||||
}),
|
||||
fail: React.PropTypes.func,
|
||||
clientSecret: React.PropTypes.func,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
matrixClient: React.PropTypes.object,
|
||||
submitAuthDict: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
token: '',
|
||||
requestingToken: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
this._tokenBox = null;
|
||||
|
||||
this.setState({requestingToken: true});
|
||||
this._requestMsisdnToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
},
|
||||
|
||||
/*
|
||||
* Requests a verification token by SMS.
|
||||
*/
|
||||
_requestMsisdnToken: function() {
|
||||
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||
this.props.inputs.phoneCountry,
|
||||
this.props.inputs.phoneNumber,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
).then((result) => {
|
||||
this._sid = result.sid;
|
||||
this._msisdn = result.msisdn;
|
||||
});
|
||||
},
|
||||
|
||||
_onTokenChange: function(e) {
|
||||
this.setState({
|
||||
token: e.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
_onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.token == '') return;
|
||||
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
|
||||
this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token
|
||||
).then((result) => {
|
||||
if (result.success) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
)
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
threepid_creds: {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
id_server: idServerParsedUrl.host,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: "Token incorrect",
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.props.fail(e);
|
||||
console.log("Failed to submit msisdn token");
|
||||
}).done();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
} else {
|
||||
const enableSubmit = Boolean(this.state.token);
|
||||
const submitClasses = classnames({
|
||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||
mx_UserSettings_button: true, // XXX button classes
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<p>A text message has been sent to +<i>{this._msisdn}</i></p>
|
||||
<p>Please enter the code it contains:</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<input type="text"
|
||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||
value={this.state.token}
|
||||
onChange={this._onTokenChange}
|
||||
/>
|
||||
<br />
|
||||
<input type="submit" value="Submit"
|
||||
className={submitClasses}
|
||||
disabled={!enableSubmit}
|
||||
/>
|
||||
</form>
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const FallbackAuthEntry = React.createClass({
|
||||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
authSessionId: React.PropTypes.string.isRequired,
|
||||
loginType: React.PropTypes.string.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
|
@ -189,7 +420,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
_onShowFallbackClick: function() {
|
||||
var url = MatrixClientPeg.get().getFallbackAuthUrl(
|
||||
var url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId
|
||||
);
|
||||
|
@ -199,7 +430,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||
_onReceiveMessage: function(event) {
|
||||
if (
|
||||
event.data === "authDone" &&
|
||||
event.origin === MatrixClientPeg.get().getHomeserverUrl()
|
||||
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||
) {
|
||||
this.props.submitAuthDict({});
|
||||
}
|
||||
|
@ -220,6 +451,8 @@ export const FallbackAuthEntry = React.createClass({
|
|||
const AuthEntryComponents = [
|
||||
PasswordAuthEntry,
|
||||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
|
||||
|
@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||
initialUsername: React.PropTypes.string,
|
||||
initialPhoneCountry: React.PropTypes.string,
|
||||
initialPhoneNumber: React.PropTypes.string,
|
||||
initialPassword: React.PropTypes.string,
|
||||
onUsernameChanged: React.PropTypes.func,
|
||||
onPhoneCountryChanged: React.PropTypes.func,
|
||||
onPhoneNumberChanged: React.PropTypes.func,
|
||||
onPasswordChanged: React.PropTypes.func,
|
||||
loginIncorrect: React.PropTypes.bool,
|
||||
},
|
||||
|
@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
return {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
};
|
||||
|
@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
return {
|
||||
username: this.props.initialUsername,
|
||||
password: this.props.initialPassword,
|
||||
phoneCountry: this.props.initialPhoneCountry,
|
||||
phoneNumber: this.props.initialPhoneNumber,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
|
||||
onSubmitForm: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onSubmit(this.state.username, this.state.password);
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
},
|
||||
|
||||
onUsernameChanged: function(ev) {
|
||||
|
@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
this.props.onUsernameChanged(ev.target.value);
|
||||
},
|
||||
|
||||
onPhoneCountryChanged: function(country) {
|
||||
this.setState({phoneCountry: country});
|
||||
this.props.onPhoneCountryChanged(country);
|
||||
},
|
||||
|
||||
onPhoneNumberChanged: function(ev) {
|
||||
this.setState({phoneNumber: ev.target.value});
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
},
|
||||
|
||||
onPasswordChanged: function(ev) {
|
||||
this.setState({password: ev.target.value});
|
||||
this.props.onPasswordChanged(ev.target.value);
|
||||
|
@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
error: this.props.loginIncorrect,
|
||||
});
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field" type="text"
|
||||
<input className="mx_Login_field mx_Login_username" type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
value={this.state.username} onChange={this.onUsernameChanged}
|
||||
placeholder="Email or user name" autoFocus />
|
||||
or
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
|
||||
className="mx_Login_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
placeholder="Mobile phone number"
|
||||
className="mx_Login_phoneNumberField mx_Login_field"
|
||||
value={this.state.phoneNumber}
|
||||
name="phoneNumber"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,18 +15,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import React from 'react';
|
||||
import { field_input_incorrect } from '../../../UiEffects';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
var React = require('react');
|
||||
var UiEffects = require('../../../UiEffects');
|
||||
var sdk = require('../../../index');
|
||||
var Email = require('../../../email');
|
||||
var Modal = require("../../../Modal");
|
||||
|
||||
var FIELD_EMAIL = 'field_email';
|
||||
var FIELD_USERNAME = 'field_username';
|
||||
var FIELD_PASSWORD = 'field_password';
|
||||
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
const FIELD_USERNAME = 'field_username';
|
||||
const FIELD_PASSWORD = 'field_password';
|
||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
|
@ -36,6 +38,8 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail: React.PropTypes.string,
|
||||
defaultPhoneCountry: React.PropTypes.string,
|
||||
defaultPhoneNumber: React.PropTypes.string,
|
||||
defaultUsername: React.PropTypes.string,
|
||||
defaultPassword: React.PropTypes.string,
|
||||
teamsConfig: React.PropTypes.shape({
|
||||
|
@ -54,15 +58,13 @@ module.exports = React.createClass({
|
|||
// a different username will cause a fresh account to be generated.
|
||||
guestUsername: React.PropTypes.string,
|
||||
|
||||
showEmail: React.PropTypes.bool,
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
onError: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showEmail: false,
|
||||
minPasswordLength: 6,
|
||||
onError: function(e) {
|
||||
console.error(e);
|
||||
|
@ -74,6 +76,8 @@ module.exports = React.createClass({
|
|||
return {
|
||||
fieldValid: {},
|
||||
selectedTeam: null,
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -88,6 +92,7 @@ module.exports = React.createClass({
|
|||
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||
this.validateField(FIELD_PASSWORD);
|
||||
this.validateField(FIELD_USERNAME);
|
||||
this.validateField(FIELD_PHONE_NUMBER);
|
||||
this.validateField(FIELD_EMAIL);
|
||||
|
||||
var self = this;
|
||||
|
@ -121,6 +126,8 @@ module.exports = React.createClass({
|
|||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
password: this.refs.password.value.trim(),
|
||||
email: email,
|
||||
phoneCountry: this.state.phoneCountry,
|
||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -174,8 +181,13 @@ module.exports = React.createClass({
|
|||
showSupportEmail: false,
|
||||
});
|
||||
}
|
||||
const valid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
const emailValid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber.value;
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
|
@ -227,7 +239,7 @@ module.exports = React.createClass({
|
|||
fieldValid[field_id] = val;
|
||||
this.setState({fieldValid: fieldValid});
|
||||
if (!val) {
|
||||
UiEffects.field_input_incorrect(this.fieldElementById(field_id));
|
||||
field_input_incorrect(this.fieldElementById(field_id));
|
||||
this.props.onError(error_code);
|
||||
}
|
||||
},
|
||||
|
@ -236,6 +248,8 @@ module.exports = React.createClass({
|
|||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
return this.refs.email;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
return this.refs.phoneNumber;
|
||||
case FIELD_USERNAME:
|
||||
return this.refs.username;
|
||||
case FIELD_PASSWORD:
|
||||
|
@ -245,8 +259,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_classForField: function(field_id, baseClass) {
|
||||
let cls = baseClass || '';
|
||||
_classForField: function(field_id, ...baseClasses) {
|
||||
let cls = baseClasses.join(' ');
|
||||
if (this.state.fieldValid[field_id] === false) {
|
||||
if (cls) cls += ' ';
|
||||
cls += 'error';
|
||||
|
@ -254,46 +268,71 @@ module.exports = React.createClass({
|
|||
return cls;
|
||||
},
|
||||
|
||||
_onPhoneCountryChange(newVal) {
|
||||
this.setState({
|
||||
phoneCountry: newVal,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var emailSection, belowEmailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
emailSection = (
|
||||
|
||||
const emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
);
|
||||
if (this.props.teamsConfig) {
|
||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
</a>
|
||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
You are registering with {this.state.selectedTeam.name}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
let belowEmailSection;
|
||||
if (this.props.teamsConfig) {
|
||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
</a>
|
||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
You are registering with {this.state.selectedTeam.name}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.props.onRegisterClick) {
|
||||
registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||
);
|
||||
}
|
||||
|
||||
var placeholderUserName = "User name";
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
const phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder="Mobile phone number (optional)"
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||
);
|
||||
|
||||
let placeholderUserName = "User name";
|
||||
if (this.props.guestUsername) {
|
||||
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
||||
}
|
||||
|
@ -303,6 +342,7 @@ module.exports = React.createClass({
|
|||
<form onSubmit={this.onSubmit}>
|
||||
{emailSection}
|
||||
{belowEmailSection}
|
||||
{phoneSection}
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
|
|
|
@ -25,6 +25,7 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -42,7 +43,7 @@ module.exports = React.createClass({
|
|||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -55,7 +56,7 @@ module.exports = React.createClass({
|
|||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
mxEvent: this.props.mxEvent
|
||||
mxEvent: this.props.mxEvent,
|
||||
};
|
||||
|
||||
if (content.info) {
|
||||
|
@ -70,22 +71,26 @@ module.exports = React.createClass({
|
|||
|
||||
_isGif: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (content && content.info && content.info.mimetype === "image/gif");
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
},
|
||||
|
||||
onImageEnter: function(e) {
|
||||
if (!this._isGif()) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
return;
|
||||
}
|
||||
var imgElement = e.target;
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getContentUrl();
|
||||
},
|
||||
|
||||
onImageLeave: function(e) {
|
||||
if (!this._isGif()) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
return;
|
||||
}
|
||||
var imgElement = e.target;
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getThumbUrl();
|
||||
},
|
||||
|
||||
|
@ -101,6 +106,7 @@ module.exports = React.createClass({
|
|||
_getThumbUrl: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||
if (this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
|
@ -115,15 +121,15 @@ module.exports = React.createClass({
|
|||
this.fixupHeight();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
var thumbnailPromise = q(null);
|
||||
let thumbnailPromise = q(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
return readBlobAsDataUri(blob);
|
||||
});
|
||||
}
|
||||
var decryptedBlob;
|
||||
let decryptedBlob;
|
||||
thumbnailPromise.then((thumbnailUrl) => {
|
||||
return decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
|
@ -168,7 +174,7 @@ module.exports = React.createClass({
|
|||
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
||||
|
||||
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
||||
var thumbHeight = null;
|
||||
let thumbHeight = null;
|
||||
if (content.info) {
|
||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
||||
}
|
||||
|
@ -190,7 +196,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
|
@ -210,7 +215,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
if (thumbUrl) {
|
||||
return (
|
||||
|
|
|
@ -23,6 +23,7 @@ import Model from '../../../Modal';
|
|||
import sdk from '../../../index';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -152,11 +153,11 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
|
||||
var height = null;
|
||||
var width = null;
|
||||
var poster = null;
|
||||
var preload = "metadata";
|
||||
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
let preload = "metadata";
|
||||
if (content.info) {
|
||||
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
||||
if (scale) {
|
||||
|
@ -169,11 +170,10 @@ module.exports = React.createClass({
|
|||
preload = "none";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<video className="mx_MVideoBody" src={contentUrl} alt={content.body}
|
||||
controls preload={preload} autoPlay={false}
|
||||
controls preload={preload} muted={autoplay} autoPlay={autoplay}
|
||||
height={height} width={width} poster={poster}>
|
||||
</video>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
|
|
|
@ -16,17 +16,18 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var highlight = require('highlight.js');
|
||||
var HtmlUtils = require('../../../HtmlUtils');
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
var sdk = require('../../../index');
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var Modal = require("../../../Modal");
|
||||
var SdkConfig = require('../../../SdkConfig');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -131,7 +132,8 @@ module.exports = React.createClass({
|
|||
links.push(node);
|
||||
}
|
||||
}
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE") {
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
node.tagName === "BLOCKQUOTE") {
|
||||
continue;
|
||||
}
|
||||
else if (node.children && node.children.length) {
|
||||
|
@ -187,6 +189,15 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onEmoteSenderClick: function(event) {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
dis.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: name.replace(' (IRC)', ''),
|
||||
});
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
var self = this;
|
||||
return {
|
||||
|
@ -273,7 +284,15 @@ module.exports = React.createClass({
|
|||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
return (
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
* <EmojiText>{name}</EmojiText> { body }
|
||||
*
|
||||
<EmojiText
|
||||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{name}
|
||||
</EmojiText>
|
||||
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -22,10 +22,10 @@ module.exports = React.createClass({
|
|||
displayName: 'UnknownBody',
|
||||
|
||||
render: function() {
|
||||
var content = this.props.mxEvent.getContent();
|
||||
const text = this.props.mxEvent.getContent().body;
|
||||
return (
|
||||
<span className="mx_UnknownBody">
|
||||
{content.body}
|
||||
<span className="mx_UnknownBody" title="Redacted or unknown message type">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -25,18 +25,10 @@ var TextForEvent = require('../../../TextForEvent');
|
|||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var dispatcher = require("../../../dispatcher");
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
var ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
var bounce = false;
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
var eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.room.member' : 'messages.TextualEvent',
|
||||
|
@ -48,6 +40,7 @@ var eventTileTypes = {
|
|||
'm.room.third_party_invite' : 'messages.TextualEvent',
|
||||
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||
'm.room.encryption' : 'messages.TextualEvent',
|
||||
'm.room.power_levels' : 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
var MAX_READ_AVATARS = 5;
|
||||
|
@ -73,6 +66,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
|
||||
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
|
||||
* references the same this.props.mxEvent.
|
||||
*/
|
||||
isRedacted: React.PropTypes.bool,
|
||||
|
||||
/* true if this is a continuation of the previous event (which has the
|
||||
* effect of not showing another avatar/displayname
|
||||
*/
|
||||
|
@ -285,9 +284,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
getReadAvatars: function() {
|
||||
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
var avatars = [];
|
||||
var left = 0;
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
const avatars = [];
|
||||
const receiptOffset = 15;
|
||||
let left = 0;
|
||||
|
||||
// It's possible that the receipt was sent several days AFTER the event.
|
||||
// If it is, we want to display the complete date along with the HH:MM:SS,
|
||||
|
@ -307,6 +307,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
|
||||
hidden = false;
|
||||
}
|
||||
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
||||
// we could optimise this to reduce the dom size.
|
||||
|
||||
// If hidden, set offset equal to the offset of the final visible avatar or
|
||||
// else set it proportional to index
|
||||
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
|
||||
|
||||
var userId = receipt.roomMember.userId;
|
||||
var readReceiptInfo;
|
||||
|
@ -319,7 +325,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
//console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility);
|
||||
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||
avatars.unshift(
|
||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||
|
@ -332,12 +337,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
showFullTimestamp={receipt.ts >= dayAfterEventTime}
|
||||
/>
|
||||
);
|
||||
|
||||
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
||||
// we could optimise this to reduce the dom size.
|
||||
if (!hidden) {
|
||||
left -= 15;
|
||||
}
|
||||
}
|
||||
var remText;
|
||||
if (!this.state.allReadAvatars) {
|
||||
|
@ -345,9 +344,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
if (remainder > 0) {
|
||||
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
style={{ left: left }}>{ remainder }+
|
||||
style={{ right: -(left - receiptOffset) }}>{ remainder }+
|
||||
</span>;
|
||||
left -= 15;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,7 +357,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
|
||||
onSenderProfileClick: function(event) {
|
||||
var mxEvent = this.props.mxEvent;
|
||||
dispatcher.dispatch({
|
||||
dis.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
||||
});
|
||||
|
@ -375,6 +373,17 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onPermalinkClicked: function(e) {
|
||||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
|
@ -399,6 +408,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
|
||||
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
|
||||
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
|
||||
var classes = classNames({
|
||||
mx_EventTile: true,
|
||||
|
@ -415,8 +425,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
mx_EventTile_verified: this.state.verified == true,
|
||||
mx_EventTile_unverified: this.state.verified == false,
|
||||
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId();
|
||||
|
||||
const permalink = "https://matrix.to/#/" +
|
||||
this.props.mxEvent.getRoomId() + "/" +
|
||||
this.props.mxEvent.getId();
|
||||
|
||||
var readAvatars = this.getReadAvatars();
|
||||
|
||||
|
@ -489,6 +503,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
else if (e2eEnabled) {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
||||
}
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
if (this.props.tileShape === "notif") {
|
||||
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
@ -496,15 +512,15 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={ permalink }>
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={ permalink }>
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line" >
|
||||
|
@ -530,10 +546,14 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
<a className="mx_EventTile_senderDetailsLink" href={ permalink }>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={ permalink }
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -548,8 +568,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={ permalink }>
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ e2e }
|
||||
<EventTileType ref="tile"
|
||||
|
@ -567,7 +587,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}));
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
if (e.isRedacted()) return false;
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
||||
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
|
|
|
@ -218,11 +218,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
onKick: function() {
|
||||
const membership = this.props.member.membership;
|
||||
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
member: this.props.member,
|
||||
action: 'Kick',
|
||||
askReason: true,
|
||||
action: kickLabel,
|
||||
askReason: membership == "join",
|
||||
danger: true,
|
||||
onFinished: (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
@ -237,9 +239,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Kick success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Kick error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to kick user",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -278,9 +281,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Ban success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Ban error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message,
|
||||
title: "Error",
|
||||
description: "Failed to ban user",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -327,9 +331,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
console.error("Mute error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to mute user",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -375,9 +380,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
description: "This action cannot be performed by a guest user. Please register to be able to do this."
|
||||
});
|
||||
} else {
|
||||
console.error("Toggle moderator error:" + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Moderator toggle error",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to toggle moderator status",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -395,9 +401,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to change power level",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to change power level",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -553,6 +560,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
},
|
||||
|
||||
_renderDevices: function() {
|
||||
if (!this._enableDevices) {
|
||||
return null;
|
||||
|
@ -569,7 +583,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
} else if (devices === null) {
|
||||
devComponents = "Unable to load device list";
|
||||
} else if (devices.length === 0) {
|
||||
devComponents = "No registered devices";
|
||||
devComponents = "No devices with registered encryption keys";
|
||||
} else {
|
||||
devComponents = [];
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
|
@ -613,6 +627,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -91,8 +91,9 @@ export default class MessageComposer extends React.Component {
|
|||
this.refs.uploadInput.click();
|
||||
}
|
||||
|
||||
onUploadFileSelected(ev) {
|
||||
let files = ev.target.files;
|
||||
onUploadFileSelected(files, isPasted) {
|
||||
if (!isPasted)
|
||||
files = files.target.files;
|
||||
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
@ -100,7 +101,7 @@ export default class MessageComposer extends React.Component {
|
|||
let fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'}
|
||||
</li>);
|
||||
}
|
||||
|
||||
|
@ -171,7 +172,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onUpArrow() {
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
}
|
||||
|
||||
onDownArrow() {
|
||||
|
@ -223,8 +224,8 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
|
||||
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (roomIsEncrypted) {
|
||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||
e2eImg = 'img/e2e-verified.svg';
|
||||
e2eTitle = 'Encrypted room';
|
||||
|
@ -286,15 +287,20 @@ export default class MessageComposer extends React.Component {
|
|||
key="controls_formatting" />
|
||||
);
|
||||
|
||||
const placeholderText = roomIsEncrypted ?
|
||||
"Send an encrypted message…" : "Send a message (unencrypted)…";
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
tryComplete={this._tryComplete}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onUploadFileSelected={this.onUploadFileSelected}
|
||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
|
|
|
@ -84,6 +84,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.onAction = this.onAction.bind(this);
|
||||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.handlePastedFiles = this.handlePastedFiles.bind(this);
|
||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||
this.setEditorState = this.setEditorState.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
|
@ -475,6 +476,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
handlePastedFiles(files) {
|
||||
this.props.onUploadFileSelected(files, true);
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if (ev.shiftKey) {
|
||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||
|
@ -504,7 +509,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error",
|
||||
description: err.message
|
||||
description: "Server unavailable, overloaded, or something else went wrong.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -536,9 +541,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendTextFn = this.client.sendTextMessage;
|
||||
|
||||
if (contentText.startsWith('/me')) {
|
||||
contentText = contentText.replace('/me', '');
|
||||
contentText = contentText.replace('/me ', '');
|
||||
// bit of a hack, but the alternative would be quite complicated
|
||||
if (contentHTML) contentHTML = contentHTML.replace('/me', '');
|
||||
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
|
||||
sendHtmlFn = this.client.sendHtmlEmote;
|
||||
sendTextFn = this.client.sendEmoteMessage;
|
||||
}
|
||||
|
@ -721,13 +726,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
placeholder={this.props.placeholder}
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onEditorContentChanged}
|
||||
blockStyleFn={MessageComposerInput.getBlockStyle}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
handlePastedFiles={this.handlePastedFiles}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
|
@ -757,6 +763,8 @@ MessageComposerInput.propTypes = {
|
|||
|
||||
onDownArrow: React.PropTypes.func,
|
||||
|
||||
onUploadFileSelected: React.PropTypes.func,
|
||||
|
||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||
tryComplete: React.PropTypes.func,
|
||||
|
||||
|
|
|
@ -34,16 +34,11 @@ export function onSendMessageFailed(err, room) {
|
|||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: room,
|
||||
onFinished: (r) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
},
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
});
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
|
@ -70,6 +65,9 @@ export default React.createClass({
|
|||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// The text to use a placeholder in the input box
|
||||
placeholder: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -313,7 +311,7 @@ export default React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error",
|
||||
description: err.message
|
||||
description: "Server unavailable, overloaded, or something else went wrong.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -442,7 +440,7 @@ export default React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -115,9 +115,10 @@ module.exports = React.createClass({
|
|||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set avatar: " + errMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
description: "Failed to set avatar.",
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
|
|
@ -96,6 +96,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'on_room_read':
|
||||
// Force an update because the notif count state is too deep to cause
|
||||
// an update. This forces the local echo of reading notifs to be
|
||||
// reflected by the RoomTiles.
|
||||
this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -485,11 +491,14 @@ module.exports = React.createClass({
|
|||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
||||
label="People"
|
||||
editable={ false }
|
||||
tagName="im.vector.fake.direct"
|
||||
verb="tag direct chat"
|
||||
editable={ true }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
alwaysShowHeader={ true }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
|
|
@ -54,9 +54,10 @@ const BannedUser = React.createClass({
|
|||
this.props.member.roomId, this.props.member.userId,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to unban: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to unban",
|
||||
description: err.message,
|
||||
title: "Error",
|
||||
description: "Failed to unban",
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var classNames = require('classnames');
|
||||
var dis = require("../../../dispatcher");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
var sdk = require('../../../index');
|
||||
|
@ -35,6 +34,7 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
connectDragSource: React.PropTypes.func,
|
||||
connectDropTarget: React.PropTypes.func,
|
||||
onClick: React.PropTypes.func,
|
||||
isDragging: React.PropTypes.bool,
|
||||
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
@ -56,8 +56,7 @@ module.exports = React.createClass({
|
|||
return({
|
||||
hover : false,
|
||||
badgeHover : false,
|
||||
notificationTagMenu: false,
|
||||
roomTagMenu: false,
|
||||
menuDisplayed: false,
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
});
|
||||
},
|
||||
|
@ -100,10 +99,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(this.props.room.roomId);
|
||||
}
|
||||
},
|
||||
|
||||
onMouseEnter: function() {
|
||||
|
@ -137,62 +135,32 @@ module.exports = React.createClass({
|
|||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu');
|
||||
var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
var elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = elementRect.right + window.pageXOffset + 3;
|
||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(NotificationStateMenu, {
|
||||
menuWidth: 188,
|
||||
menuHeight: 126,
|
||||
chevronOffset: 45,
|
||||
ContextualMenu.createMenu(RoomTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ notificationTagMenu: false });
|
||||
self.setState({ menuDisplayed: false });
|
||||
self.props.refreshSubList();
|
||||
}
|
||||
});
|
||||
this.setState({ notificationTagMenu: true });
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
onAvatarClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest() && !this.props.collapsed) {
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
var RoomTagMenu = sdk.getComponent('context_menus.RoomTagContextMenu');
|
||||
var elementRect = e.target.getBoundingClientRect();
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = elementRect.right + window.pageXOffset + 3;
|
||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 19;
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(RoomTagMenu, {
|
||||
chevronOffset: 10,
|
||||
// XXX: fix horrid hardcoding
|
||||
menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF",
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ roomTagMenu: false });
|
||||
}
|
||||
});
|
||||
this.setState({ roomTagMenu: true });
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var me = this.props.room.currentState.members[myUserId];
|
||||
|
@ -211,7 +179,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
});
|
||||
|
||||
|
@ -219,14 +187,9 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_avatar': true,
|
||||
});
|
||||
|
||||
var avatarContainerClasses = classNames({
|
||||
'mx_RoomTile_avatar_container': true,
|
||||
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.notificationTagMenu,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
// XXX: We should never display raw room IDs, but sometimes the
|
||||
|
@ -237,7 +200,7 @@ module.exports = React.createClass({
|
|||
var badge;
|
||||
var badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.notificationTagMenu) {
|
||||
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||
badgeContent = "\u00B7\u00B7\u00B7";
|
||||
} else if (badges) {
|
||||
var limitedCount = FormattingUtils.formatCount(notificationCount);
|
||||
|
@ -255,7 +218,7 @@ module.exports = React.createClass({
|
|||
var nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.props.isInvite,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
if (this.props.selected) {
|
||||
|
@ -294,11 +257,9 @@ module.exports = React.createClass({
|
|||
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
||||
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
||||
<div className={avatarContainerClasses}>
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{directMessageIndicator}
|
||||
</div>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{directMessageIndicator}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
|
|
|
@ -32,10 +32,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_TopUnreadMessagesBar">
|
||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
<img src="img/scrollup.svg" width="24" height="24"
|
||||
alt="Scroll to unread messages"
|
||||
title="Scroll to unread messages"/>
|
||||
Unread messages. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
||||
Jump to first unread message. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
||||
</div>
|
||||
<img className="mx_TopUnreadMessagesBar_close"
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
|
||||
export default WithMatrixClient(React.createClass({
|
||||
displayName: 'AddPhoneNumber',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
onThreepidAdded: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
msisdn_add_pending: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._addThreepid = null;
|
||||
this._addMsisdnInput = null;
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_onPhoneCountryChange: function(phoneCountry) {
|
||||
this.setState({ phoneCountry: phoneCountry });
|
||||
},
|
||||
|
||||
_onPhoneNumberChange: function(ev) {
|
||||
this.setState({ phoneNumber: ev.target.value });
|
||||
},
|
||||
|
||||
_onAddMsisdnEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
this._addMsisdn();
|
||||
},
|
||||
|
||||
_onAddMsisdnSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this._addMsisdn();
|
||||
},
|
||||
|
||||
_collectAddMsisdnInput: function(e) {
|
||||
this._addMsisdnInput = e;
|
||||
},
|
||||
|
||||
_addMsisdn: function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
this._addThreepid = new AddThreepid();
|
||||
// we always bind phone numbers when registering, so let's do the
|
||||
// same here.
|
||||
this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => {
|
||||
this._promptForMsisdnVerificationCode(resp.msisdn);
|
||||
}).catch((err) => {
|
||||
console.error("Unable to add phone number: " + err);
|
||||
let msg = err.message;
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: msg,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: false});
|
||||
}).done();
|
||||
this._addMsisdnInput.blur();
|
||||
this.setState({msisdn_add_pending: true});
|
||||
},
|
||||
|
||||
_promptForMsisdnVerificationCode:function (msisdn, err) {
|
||||
if (this._unmounted) return;
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
let msgElements = [
|
||||
<div key="_static" >A text message has been sent to +{msisdn}.
|
||||
Please enter the verification code it contains</div>
|
||||
];
|
||||
if (err) {
|
||||
let msg = err.error;
|
||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||
msg = "Incorrect verification code";
|
||||
}
|
||||
msgElements.push(<div key="_error" className="error">{msg}</div>);
|
||||
}
|
||||
Modal.createDialog(TextInputDialog, {
|
||||
title: "Enter Code",
|
||||
description: <div>{msgElements}</div>,
|
||||
button: "Submit",
|
||||
onFinished: (should_verify, token) => {
|
||||
if (!should_verify) {
|
||||
this._addThreepid = null;
|
||||
return;
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: true});
|
||||
this._addThreepid.haveMsisdnToken(token).then(() => {
|
||||
this._addThreepid = null;
|
||||
this.setState({phoneNumber: ''});
|
||||
if (this.props.onThreepidAdded) this.props.onThreepidAdded();
|
||||
}).catch((err) => {
|
||||
this._promptForMsisdnVerificationCode(msisdn, err);
|
||||
}).finally(() => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: false});
|
||||
}).done();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.state.msisdn_add_pending) {
|
||||
return <Loader />;
|
||||
} else if (this.props.matrixClient.isGuest()) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
// XXX: This CSS relies on the CSS surrounding it in UserSettings as its in
|
||||
// a tabular format to align the submit buttons
|
||||
return (
|
||||
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
/>
|
||||
<input type="text"
|
||||
ref={this._collectAddMsisdnInput}
|
||||
className="mx_UserSettings_phoneNumberField"
|
||||
placeholder="Add phone number"
|
||||
value={this.state.phoneNumber}
|
||||
onChange={this._onPhoneNumberChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<input type="image" value="Add" src="img/plus.svg" width="14" height="14" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}))
|
|
@ -53,6 +53,7 @@ module.exports = React.createClass({
|
|||
<EditableTextContainer
|
||||
getInitialValue={this._getDisplayName}
|
||||
placeholder="No display name"
|
||||
blurToSubmit={true}
|
||||
onSubmit={this._changeDisplayName} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,11 +73,17 @@ module.exports = React.createClass({
|
|||
description:
|
||||
<div>
|
||||
Changing password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
This will be <a href="https://github.com/vector-im/riot-web/issues/2671">improved shortly</a>,
|
||||
but for now be warned.
|
||||
making encrypted chat history unreadable, unless you first export your room keys
|
||||
and re-import them afterwards.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
var authDict = {
|
||||
|
@ -105,6 +111,18 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onClickChange: function() {
|
||||
var old_password = this.refs.old_input.value;
|
||||
var new_password = this.refs.new_input.value;
|
||||
|
|
|
@ -19,6 +19,9 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import DateUtils from '../../../DateUtils';
|
||||
|
||||
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
@ -30,7 +33,6 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
};
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
|
||||
|
@ -53,8 +55,12 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
_onDeleteClick() {
|
||||
this.setState({deleting: true});
|
||||
|
||||
// try without interactive auth to start off
|
||||
this._makeDeleteRequest(null).catch((error) => {
|
||||
if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) {
|
||||
this.context.authCache.auth = null;
|
||||
}
|
||||
|
||||
// try with auth cache (which is null, so no interactive auth, to start off)
|
||||
this._makeDeleteRequest(this.context.authCache.auth).catch((error) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
|
@ -65,6 +71,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
Modal.createDialog(InteractiveAuthDialog, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: error.data,
|
||||
makeRequest: this._makeDeleteRequest,
|
||||
});
|
||||
|
@ -83,6 +90,9 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
}
|
||||
|
||||
_makeDeleteRequest(auth) {
|
||||
this.context.authCache.auth = auth;
|
||||
this.context.authCache.lastUpdate = Date.now();
|
||||
|
||||
const device = this.props.device;
|
||||
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
|
||||
() => {
|
||||
|
@ -110,8 +120,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
|
||||
let lastSeen = "";
|
||||
if (device.last_seen_ts) {
|
||||
// todo: format the timestamp as "5 minutes ago" or whatever.
|
||||
const lastSeenDate = new Date(device.last_seen_ts);
|
||||
const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts));
|
||||
lastSeen = device.last_seen_ip + " @ " +
|
||||
lastSeenDate.toLocaleString();
|
||||
}
|
||||
|
@ -160,6 +169,10 @@ DevicesPanelEntry.propTypes = {
|
|||
onDeleted: React.PropTypes.func,
|
||||
};
|
||||
|
||||
DevicesPanelEntry.contextTypes = {
|
||||
authCache: React.PropTypes.object,
|
||||
};
|
||||
|
||||
DevicesPanelEntry.defaultProps = {
|
||||
onDeleted: function() {},
|
||||
};
|
||||
|
|
|
@ -102,9 +102,10 @@ function createRoom(opts) {
|
|||
});
|
||||
return roomId;
|
||||
}, function(err) {
|
||||
console.error("Failed to create room " + roomId + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to create room",
|
||||
description: err.toString()
|
||||
description: "Server may be unavailable, overloaded, or you hit a bug.",
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
|
|||
// anyone else really should be using matrix.to.
|
||||
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||
+ escapeRegExp(window.location.host + window.location.pathname) + "|"
|
||||
+ "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/"
|
||||
+ "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/"
|
||||
+ ")(#.*)";
|
||||
|
||||
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -57,6 +57,7 @@ describe('InteractiveAuthDialog', function () {
|
|||
|
||||
const dlg = ReactDOM.render(
|
||||
<InteractiveAuthDialog
|
||||
matrixClient={client}
|
||||
authData={{
|
||||
session: "sess",
|
||||
flows: [
|
||||
|
@ -67,48 +68,49 @@ describe('InteractiveAuthDialog', function () {
|
|||
onFinished={onFinished}
|
||||
/>, parentDiv);
|
||||
|
||||
// at this point there should be a password box and a submit button
|
||||
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
|
||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||
dlg, "input"
|
||||
);
|
||||
let passwordNode;
|
||||
let submitNode;
|
||||
for (const node of inputNodes) {
|
||||
if (node.type == 'password') {
|
||||
passwordNode = node;
|
||||
} else if (node.type == 'submit') {
|
||||
submitNode = node;
|
||||
// wait for a password box and a submit button
|
||||
test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => {
|
||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||
dlg, "input"
|
||||
);
|
||||
let passwordNode;
|
||||
let submitNode;
|
||||
for (const node of inputNodes) {
|
||||
if (node.type == 'password') {
|
||||
passwordNode = node;
|
||||
} else if (node.type == 'submit') {
|
||||
submitNode = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(passwordNode).toExist();
|
||||
expect(submitNode).toExist();
|
||||
expect(passwordNode).toExist();
|
||||
expect(submitNode).toExist();
|
||||
|
||||
// submit should be disabled
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
// submit should be disabled
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
|
||||
// put something in the password box, and hit enter; that should
|
||||
// trigger a request
|
||||
passwordNode.value = "s3kr3t";
|
||||
ReactTestUtils.Simulate.change(passwordNode);
|
||||
expect(submitNode.disabled).toBe(false);
|
||||
ReactTestUtils.Simulate.submit(formNode, {});
|
||||
// put something in the password box, and hit enter; that should
|
||||
// trigger a request
|
||||
passwordNode.value = "s3kr3t";
|
||||
ReactTestUtils.Simulate.change(passwordNode);
|
||||
expect(submitNode.disabled).toBe(false);
|
||||
ReactTestUtils.Simulate.submit(formNode, {});
|
||||
|
||||
expect(doRequest.callCount).toEqual(1);
|
||||
expect(doRequest.calledWithExactly({
|
||||
session: "sess",
|
||||
type: "m.login.password",
|
||||
password: "s3kr3t",
|
||||
user: "@user:id",
|
||||
})).toBe(true);
|
||||
expect(doRequest.callCount).toEqual(1);
|
||||
expect(doRequest.calledWithExactly({
|
||||
session: "sess",
|
||||
type: "m.login.password",
|
||||
password: "s3kr3t",
|
||||
user: "@user:id",
|
||||
})).toBe(true);
|
||||
|
||||
// there should now be a spinner
|
||||
ReactTestUtils.findRenderedComponentWithType(
|
||||
dlg, sdk.getComponent('elements.Spinner'),
|
||||
);
|
||||
// there should now be a spinner
|
||||
ReactTestUtils.findRenderedComponentWithType(
|
||||
dlg, sdk.getComponent('elements.Spinner'),
|
||||
);
|
||||
|
||||
// let the request complete
|
||||
q.delay(1).then(() => {
|
||||
// let the request complete
|
||||
return q.delay(1);
|
||||
}).then(() => {
|
||||
expect(onFinished.callCount).toEqual(1);
|
||||
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
||||
}).done(done, done);
|
||||
|
|
|
@ -1,11 +1,51 @@
|
|||
"use strict";
|
||||
|
||||
var sinon = require('sinon');
|
||||
var q = require('q');
|
||||
import sinon from 'sinon';
|
||||
import q from 'q';
|
||||
import ReactTestUtils from 'react-addons-test-utils';
|
||||
|
||||
var peg = require('../src/MatrixClientPeg.js');
|
||||
var jssdk = require('matrix-js-sdk');
|
||||
var MatrixEvent = jssdk.MatrixEvent;
|
||||
import peg from '../src/MatrixClientPeg.js';
|
||||
import jssdk from 'matrix-js-sdk';
|
||||
const MatrixEvent = jssdk.MatrixEvent;
|
||||
|
||||
/**
|
||||
* Wrapper around window.requestAnimationFrame that returns a promise
|
||||
* @private
|
||||
*/
|
||||
function _waitForFrame() {
|
||||
const def = q.defer();
|
||||
window.requestAnimationFrame(() => {
|
||||
def.resolve();
|
||||
});
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits a small number of animation frames for a component to appear
|
||||
* in the DOM. Like findRenderedDOMComponentWithTag(), but allows
|
||||
* for the element to appear a short time later, eg. if a promise needs
|
||||
* to resolve first.
|
||||
* @return a promise that resolves once the component appears, or rejects
|
||||
* if it doesn't appear after a nominal number of animation frames.
|
||||
*/
|
||||
export function waitForRenderedDOMComponentWithTag(tree, tag, attempts) {
|
||||
if (attempts === undefined) {
|
||||
// Let's start by assuming we'll only need to wait a single frame, and
|
||||
// we can try increasing this if necessary.
|
||||
attempts = 1;
|
||||
} else if (attempts == 0) {
|
||||
return q.reject("Gave up waiting for component with tag: " + tag);
|
||||
}
|
||||
|
||||
return _waitForFrame().then(() => {
|
||||
const result = ReactTestUtils.scryRenderedDOMComponentsWithTag(tree, tag);
|
||||
if (result.length > 0) {
|
||||
return result[0];
|
||||
} else {
|
||||
return waitForRenderedDOMComponentWithTag(tree, tag, attempts - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform common actions before each test case, e.g. printing the test case
|
||||
|
@ -92,6 +132,7 @@ export function createTestClient() {
|
|||
sendTextMessage: () => q({}),
|
||||
sendHtmlMessage: () => q({}),
|
||||
getSyncState: () => "SYNCING",
|
||||
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue