mirror of
https://github.com/vector-im/element-web.git
synced 2025-08-23 07:21:22 +02:00
Merge branch 'develop' into matthew/whitelist-uri-schemes
This commit is contained in:
commit
ec2a5cce74
23
.editorconfig
Normal file
23
.editorconfig
Normal file
@ -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)
|
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)
|
[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",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.6",
|
"version": "0.8.7",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -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
|
* 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
|
* 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() {
|
checkEmailLinkClicked() {
|
||||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
@ -73,6 +99,29 @@ class AddThreepid {
|
|||||||
throw err;
|
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;
|
module.exports = AddThreepid;
|
||||||
|
@ -82,4 +82,12 @@ export default class BasePlatform {
|
|||||||
screenCaptureErrorString() {
|
screenCaptureErrorString() {
|
||||||
return "Not implemented";
|
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();
|
call.hangup();
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
_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() {
|
call.on("hangup", function() {
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
_setCallState(undefined, call.roomId, "ended");
|
||||||
});
|
});
|
||||||
@ -301,9 +310,10 @@ function _onAction(payload) {
|
|||||||
placeCall(call);
|
placeCall(call);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Conference call failed: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to set up conference call",
|
title: "Failed to set up conference call",
|
||||||
description: "Conference call failed: " + err,
|
description: "Conference call failed.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ class ContentMessages {
|
|||||||
|
|
||||||
sendContentToRoom(file, roomId, matrixClient) {
|
sendContentToRoom(file, roomId, matrixClient) {
|
||||||
const content = {
|
const content = {
|
||||||
body: file.name,
|
body: file.name || 'Attachment',
|
||||||
info: {
|
info: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
}
|
}
|
||||||
@ -316,7 +316,7 @@ class ContentMessages {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const upload = {
|
const upload = {
|
||||||
fileName: file.name,
|
fileName: file.name || 'Attachment',
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
total: 0,
|
total: 0,
|
||||||
loaded: 0,
|
loaded: 0,
|
||||||
|
@ -28,6 +28,7 @@ emojione.imagePathSVG = 'emojione/svg/';
|
|||||||
emojione.imageType = 'svg';
|
emojione.imageType = 'svg';
|
||||||
|
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
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
|
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||||
* because we want to include emoji shortnames in title text
|
* because we want to include emoji shortnames in title text
|
||||||
@ -57,6 +58,22 @@ export function unicodeToImage(str) {
|
|||||||
return 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 {
|
export function stripParagraphs(html: string): string {
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
@ -87,15 +104,17 @@ var sanitizeHtmlParams = {
|
|||||||
// deliberately no h1/h2 to stop people shouting.
|
// deliberately no h1/h2 to stop people shouting.
|
||||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
'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: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// 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
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
// We don't currently allow img itself by default, but this
|
// We don't currently allow img itself by default, but this
|
||||||
// would make sense if we did
|
// would make sense if we did
|
||||||
img: ['src'],
|
img: ['src'],
|
||||||
|
ol: ['start'],
|
||||||
},
|
},
|
||||||
// Lots of these won't come up by default because we don't allow them
|
// 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'],
|
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/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName: tagName, attribs : attribs };
|
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+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
// We allow localhost for mxids to avoid confusion
|
const mxidRegex = /^@\S+:\S+$/
|
||||||
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
|
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText) {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
const isEmailAddress = emailRegex.test(inputText);
|
||||||
|
@ -155,7 +155,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
|||||||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
console.log("Doing guest login on %s", hsUrl);
|
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.
|
// Not really sure where the right home for it is.
|
||||||
|
|
||||||
// create a temporary MatrixClient to do the login
|
// create a temporary MatrixClient to do the login
|
||||||
@ -276,6 +276,14 @@ export function setLoggedIn(credentials) {
|
|||||||
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
||||||
credentials.userId, credentials.guest,
|
credentials.userId, credentials.guest,
|
||||||
credentials.homeserverUrl);
|
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
|
// persist the session
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
@ -300,25 +308,29 @@ export function setLoggedIn(credentials) {
|
|||||||
console.warn("Error using local storage: can't persist session!", e);
|
console.warn("Error using local storage: can't persist session!", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rtsClient) {
|
if (rtsClient && !credentials.guest) {
|
||||||
rtsClient.login(credentials.userId).then((body) => {
|
teamPromise = rtsClient.login(credentials.userId).then((body) => {
|
||||||
if (body.team_token) {
|
if (body.team_token) {
|
||||||
localStorage.setItem("mx_team_token", body.team_token);
|
localStorage.setItem("mx_team_token", body.team_token);
|
||||||
}
|
}
|
||||||
}, (err) =>{
|
return body.team_token;
|
||||||
console.error(
|
|
||||||
"Failed to get team token on login, not persisting to localStorage",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("No local storage available: can't persist session!");
|
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);
|
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();
|
startMatrixClient();
|
||||||
}
|
}
|
||||||
|
205
src/Login.js
Normal file
205
src/Login.js
Normal file
@ -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() {
|
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;
|
const real_paragraph = renderer.paragraph;
|
||||||
|
|
||||||
renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node, entering) {
|
||||||
|
@ -50,6 +50,18 @@ class MatrixClientPeg {
|
|||||||
this.opts = {
|
this.opts = {
|
||||||
initialSyncLimit: 20,
|
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 {
|
get(): MatrixClient {
|
||||||
@ -122,12 +134,15 @@ class MatrixClientPeg {
|
|||||||
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
||||||
}
|
}
|
||||||
if (window.indexedDB && localStorage) {
|
if (window.indexedDB && localStorage) {
|
||||||
opts.store = new Matrix.IndexedDBStore(
|
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
||||||
new Matrix.IndexedDBStoreBackend(window.indexedDB),
|
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
||||||
new Matrix.SyncAccumulator(), {
|
|
||||||
localStorage: localStorage,
|
opts.store = new Matrix.IndexedDBStore({
|
||||||
}
|
indexedDB: window.indexedDB,
|
||||||
);
|
dbName: "riot-web-sync",
|
||||||
|
localStorage: localStorage,
|
||||||
|
workerScript: this.indexedDbWorkerScript,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.matrixClient = Matrix.createClient(opts);
|
this.matrixClient = Matrix.createClient(opts);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var PlatformPeg = require("./PlatformPeg");
|
var PlatformPeg = require("./PlatformPeg");
|
||||||
var TextForEvent = require('./TextForEvent');
|
var TextForEvent = require('./TextForEvent');
|
||||||
@ -99,16 +98,16 @@ var Notifier = {
|
|||||||
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
this.toolbarHidden = false;
|
this.toolbarHidden = false;
|
||||||
this.isPrepared = false;
|
this.isSyncing = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
}
|
}
|
||||||
this.isPrepared = false;
|
this.isSyncing = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
supportsDesktopNotifications: function() {
|
supportsDesktopNotifications: function() {
|
||||||
@ -214,18 +213,18 @@ var Notifier = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSyncStateChange: function(state) {
|
onSyncStateChange: function(state) {
|
||||||
if (state === "PREPARED" || state === "SYNCING") {
|
if (state === "SYNCING") {
|
||||||
this.isPrepared = true;
|
this.isSyncing = true;
|
||||||
}
|
}
|
||||||
else if (state === "STOPPED" || state === "ERROR") {
|
else if (state === "STOPPED" || state === "ERROR") {
|
||||||
this.isPrepared = false;
|
this.isSyncing = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||||
if (toStartOfTimeline) return;
|
if (toStartOfTimeline) return;
|
||||||
if (!room) 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 (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
|
||||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||||
|
|
||||||
|
@ -18,11 +18,27 @@ var MatrixClientPeg = require('./MatrixClientPeg');
|
|||||||
var dis = require('./dispatcher');
|
var dis = require('./dispatcher');
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
var Modal = require('./Modal');
|
var Modal = require('./Modal');
|
||||||
|
import { EventStatus } from 'matrix-js-sdk';
|
||||||
|
|
||||||
module.exports = {
|
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) {
|
resend: function(event) {
|
||||||
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
MatrixClientPeg.get().resendEvent(
|
MatrixClientPeg.get().resendEvent(
|
||||||
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
event, room
|
||||||
).done(function(res) {
|
).done(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
@ -33,16 +49,11 @@ module.exports = {
|
|||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
||||||
if (err.name === "UnknownDeviceError") {
|
if (err.name === "UnknownDeviceError") {
|
||||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
dis.dispatch({
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
action: 'unknown_device_error',
|
||||||
devices: err.devices,
|
err: err,
|
||||||
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
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({
|
dis.dispatch({
|
||||||
@ -51,7 +62,6 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFromQueue: function(event) {
|
removeFromQueue: function(event) {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
29
src/Roles.js
Normal file
29
src/Roles.js
Normal file
@ -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;
|
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.
|
* Marks or unmarks the given room as being as a DM room.
|
||||||
* @param {string} roomId The ID of the room to modify
|
* @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 MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var CallHandler = require("./CallHandler");
|
var CallHandler = require("./CallHandler");
|
||||||
|
|
||||||
|
import * as Roles from './Roles';
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
function textForMemberEvent(ev) {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
@ -116,7 +118,6 @@ function textForRoomNameEvent(ev) {
|
|||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
var message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
@ -183,6 +184,45 @@ function textForEncryptionEvent(event) {
|
|||||||
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")";
|
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 = {
|
var handlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.room.name': textForRoomNameEvent,
|
||||||
@ -194,6 +234,7 @@ var handlers = {
|
|||||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||||
'm.room.encryption': textForEncryptionEvent,
|
'm.room.encryption': textForEncryptionEvent,
|
||||||
|
'm.room.power_levels': textForPowerEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
51
src/UnknownDeviceErrorHandler.js
Normal file
51
src/UnknownDeviceErrorHandler.js
Normal file
@ -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);
|
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
||||||
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||||
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = 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';
|
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = 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';
|
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
|
||||||
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
||||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
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);
|
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
||||||
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
||||||
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = 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';
|
import views$elements$EditableText from './components/views/elements/EditableText';
|
||||||
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
||||||
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
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);
|
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
||||||
import views$login$CasLogin from './components/views/login/CasLogin';
|
import views$login$CasLogin from './components/views/login/CasLogin';
|
||||||
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = 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';
|
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
||||||
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
||||||
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
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);
|
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
|
||||||
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
||||||
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = 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';
|
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
|
||||||
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
||||||
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
||||||
|
@ -27,6 +27,9 @@ export default React.createClass({
|
|||||||
displayName: 'InteractiveAuth',
|
displayName: 'InteractiveAuth',
|
||||||
|
|
||||||
propTypes: {
|
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
|
// response from initial request. If not supplied, will do a request on
|
||||||
// mount.
|
// mount.
|
||||||
authData: React.PropTypes.shape({
|
authData: React.PropTypes.shape({
|
||||||
@ -38,11 +41,34 @@ export default React.createClass({
|
|||||||
// callback
|
// callback
|
||||||
makeRequest: React.PropTypes.func.isRequired,
|
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
|
// @param {bool} status True if the operation requiring
|
||||||
// auth was completed sucessfully, false if canceled.
|
// auth was completed sucessfully, false if canceled.
|
||||||
// @param result The result of the authenticated call
|
// @param {object} result The result of the authenticated call
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
// 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() {
|
getInitialState: function() {
|
||||||
@ -60,12 +86,22 @@ export default React.createClass({
|
|||||||
this._authLogic = new InteractiveAuth({
|
this._authLogic = new InteractiveAuth({
|
||||||
authData: this.props.authData,
|
authData: this.props.authData,
|
||||||
doRequest: this._requestCallback,
|
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._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) => {
|
}).catch((error) => {
|
||||||
|
this.props.onAuthFinished(false, error);
|
||||||
console.error("Error during user-interactive auth:", error);
|
console.error("Error during user-interactive auth:", error);
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
@ -76,26 +112,48 @@ export default React.createClass({
|
|||||||
errorText: msg
|
errorText: msg
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
|
|
||||||
|
this._intervalId = null;
|
||||||
|
if (this.props.poll) {
|
||||||
|
this._intervalId = setInterval(() => {
|
||||||
|
this._authLogic.poll();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this._unmounted = true;
|
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({
|
this.setState({
|
||||||
authStage: stageType,
|
authStage: stageType,
|
||||||
errorText: error ? error.error : null,
|
stageState: stageState,
|
||||||
}, this._setFocus);
|
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({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
stageErrorText: null,
|
stageErrorText: null,
|
||||||
});
|
});
|
||||||
return this.props.makeRequest(auth).finally(() => {
|
return makeRequestPromise.finally(() => {
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -117,19 +175,35 @@ export default React.createClass({
|
|||||||
|
|
||||||
_renderCurrentStage: function() {
|
_renderCurrentStage: function() {
|
||||||
const stage = this.state.authStage;
|
const stage = this.state.authStage;
|
||||||
var StageComponent = getEntryComponentForLoginType(stage);
|
if (!stage) return null;
|
||||||
|
|
||||||
|
const StageComponent = getEntryComponentForLoginType(stage);
|
||||||
return (
|
return (
|
||||||
<StageComponent ref="stageComponent"
|
<StageComponent ref="stageComponent"
|
||||||
loginType={stage}
|
loginType={stage}
|
||||||
|
matrixClient={this.props.matrixClient}
|
||||||
authSessionId={this._authLogic.getSessionId()}
|
authSessionId={this._authLogic.getSessionId()}
|
||||||
|
clientSecret={this._authLogic.getClientSecret()}
|
||||||
stageParams={this._authLogic.getStageParams(stage)}
|
stageParams={this._authLogic.getStageParams(stage)}
|
||||||
submitAuthDict={this._submitAuthDict}
|
submitAuthDict={this._submitAuthDict}
|
||||||
errorText={this.state.stageErrorText}
|
errorText={this.state.stageErrorText}
|
||||||
busy={this.state.busy}
|
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() {
|
render: function() {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
|
@ -49,11 +49,16 @@ export default React.createClass({
|
|||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
||||||
|
authCache: React.PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
getChildContext: function() {
|
getChildContext: function() {
|
||||||
return {
|
return {
|
||||||
matrixClient: this._matrixClient,
|
matrixClient: this._matrixClient,
|
||||||
|
authCache: {
|
||||||
|
auth: {},
|
||||||
|
lastUpdate: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -76,6 +81,13 @@ export default React.createClass({
|
|||||||
return this._scrollStateMap[roomId];
|
return this._scrollStateMap[roomId];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canResetTimelineInRoom: function(roomId) {
|
||||||
|
if (!this.refs.roomView) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.refs.roomView.canResetTimeline();
|
||||||
|
},
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
/*
|
/*
|
||||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||||
@ -94,6 +106,17 @@ export default React.createClass({
|
|||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
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.UP:
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
if (ev.altKey) {
|
if (ev.altKey) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -28,10 +29,6 @@ var UserActivity = require("../../UserActivity");
|
|||||||
var Presence = require("../../Presence");
|
var Presence = require("../../Presence");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
|
|
||||||
var Login = require("./login/Login");
|
|
||||||
var Registration = require("./login/Registration");
|
|
||||||
var PostRegistration = require("./login/PostRegistration");
|
|
||||||
|
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
var Tinter = require("../../Tinter");
|
var Tinter = require("../../Tinter");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
@ -41,6 +38,7 @@ var Lifecycle = require('../../Lifecycle');
|
|||||||
var PageTypes = require('../../PageTypes');
|
var PageTypes = require('../../PageTypes');
|
||||||
|
|
||||||
var createRoom = require("../../createRoom");
|
var createRoom = require("../../createRoom");
|
||||||
|
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MatrixChat',
|
displayName: 'MatrixChat',
|
||||||
@ -61,9 +59,19 @@ module.exports = React.createClass({
|
|||||||
// called when the session load completes
|
// called when the session load completes
|
||||||
onLoadCompleted: React.PropTypes.func,
|
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
|
// displayname, if any, to set on the device when logging
|
||||||
// in/registering.
|
// in/registering.
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
|
// A function that makes a registration URL
|
||||||
|
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
@ -84,6 +92,12 @@ module.exports = React.createClass({
|
|||||||
var s = {
|
var s = {
|
||||||
loading: true,
|
loading: true,
|
||||||
screen: undefined,
|
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
|
// What the LoggedInView would be showing if visible
|
||||||
page_type: null,
|
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
|
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
|
|
||||||
logged_in: false,
|
loggedIn: false,
|
||||||
|
loggingIn: false,
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
@ -179,13 +194,9 @@ module.exports = React.createClass({
|
|||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
SdkConfig.put(this.props.config);
|
SdkConfig.put(this.props.config);
|
||||||
|
|
||||||
// Stashed guest credentials if the user logs out
|
// Used by _viewRoom before getting state from sync
|
||||||
// whilst logged in as a guest user (so they can change
|
this.firstSyncComplete = false;
|
||||||
// their mind & log back in)
|
this.firstSyncPromise = q.defer();
|
||||||
this.guestCreds = null;
|
|
||||||
|
|
||||||
// if the automatic session load failed, the error
|
|
||||||
this.sessionLoadError = null;
|
|
||||||
|
|
||||||
if (this.props.config.sync_timeline_limit) {
|
if (this.props.config.sync_timeline_limit) {
|
||||||
MatrixClientPeg.opts.initialSyncLimit = 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) {
|
if (this._teamToken) {
|
||||||
console.info(`Team token set to ${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() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
UDEHandler.startListening();
|
||||||
|
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
window.addEventListener("focus", this.onFocus);
|
window.addEventListener("focus", this.onFocus);
|
||||||
@ -265,7 +286,6 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.error("Unable to load session", e);
|
console.error("Unable to load session", e);
|
||||||
this.sessionLoadError = e.message;
|
|
||||||
}).done(()=>{
|
}).done(()=>{
|
||||||
// stuff this through the dispatcher so that it happens
|
// stuff this through the dispatcher so that it happens
|
||||||
// after the on_logged_in action.
|
// after the on_logged_in action.
|
||||||
@ -276,6 +296,7 @@ module.exports = React.createClass({
|
|||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
Lifecycle.stopMatrixClient();
|
Lifecycle.stopMatrixClient();
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
UDEHandler.stopListening();
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
@ -291,7 +312,7 @@ module.exports = React.createClass({
|
|||||||
const newState = {
|
const newState = {
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
logged_in: false,
|
loggedIn: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
upgradeUsername: null,
|
upgradeUsername: null,
|
||||||
guestAccessToken: null,
|
guestAccessToken: null,
|
||||||
@ -301,88 +322,123 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var roomIndexDelta = 1;
|
var roomIndexDelta = 1;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
|
||||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
|
||||||
}
|
|
||||||
Lifecycle.logout();
|
Lifecycle.logout();
|
||||||
break;
|
break;
|
||||||
case 'start_registration':
|
case 'start_registration':
|
||||||
var newState = payload.params || {};
|
const params = payload.params || {};
|
||||||
newState.screen = 'register';
|
this.setStateForNewScreen({
|
||||||
if (
|
screen: 'register',
|
||||||
payload.params &&
|
// these params may be undefined, but if they are,
|
||||||
payload.params.client_secret &&
|
// unset them from our state: we don't want to
|
||||||
payload.params.session_id &&
|
// resume a previous registration session if the
|
||||||
payload.params.hs_url &&
|
// user just clicked 'register'
|
||||||
payload.params.is_url &&
|
register_client_secret: params.client_secret,
|
||||||
payload.params.sid
|
register_session_id: params.session_id,
|
||||||
) {
|
register_hs_url: params.hs_url,
|
||||||
newState.register_client_secret = payload.params.client_secret;
|
register_is_url: params.is_url,
|
||||||
newState.register_session_id = payload.params.session_id;
|
register_id_sid: params.sid,
|
||||||
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);
|
|
||||||
this.notifyNewScreen('register');
|
this.notifyNewScreen('register');
|
||||||
break;
|
break;
|
||||||
case 'start_login':
|
case 'start_login':
|
||||||
if (this.state.logged_in) return;
|
if (MatrixClientPeg.get() &&
|
||||||
|
MatrixClientPeg.get().isGuest()
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
guestCreds: MatrixClientPeg.getCredentials(),
|
||||||
|
});
|
||||||
|
}
|
||||||
this.setStateForNewScreen({
|
this.setStateForNewScreen({
|
||||||
screen: 'login',
|
screen: 'login',
|
||||||
});
|
});
|
||||||
this.notifyNewScreen('login');
|
this.notifyNewScreen('login');
|
||||||
break;
|
break;
|
||||||
case 'start_post_registration':
|
case 'start_post_registration':
|
||||||
this.setState({ // don't clobber logged_in status
|
this.setState({ // don't clobber loggedIn status
|
||||||
screen: 'post_registration'
|
screen: 'post_registration'
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'start_upgrade_registration':
|
case 'start_upgrade_registration':
|
||||||
// stash our guest creds so we can backout if needed
|
// also stash our credentials, then if we restore the session,
|
||||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
// we can just do it the same way whether we started upgrade
|
||||||
|
// registration or explicitly logged out
|
||||||
this.setStateForNewScreen({
|
this.setStateForNewScreen({
|
||||||
|
guestCreds: MatrixClientPeg.getCredentials(),
|
||||||
screen: "register",
|
screen: "register",
|
||||||
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||||
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
|
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');
|
this.notifyNewScreen('register');
|
||||||
break;
|
break;
|
||||||
case 'start_password_recovery':
|
case 'start_password_recovery':
|
||||||
if (this.state.logged_in) return;
|
if (this.state.loggedIn) return;
|
||||||
this.setStateForNewScreen({
|
this.setStateForNewScreen({
|
||||||
screen: 'forgot_password',
|
screen: 'forgot_password',
|
||||||
});
|
});
|
||||||
this.notifyNewScreen('forgot_password');
|
this.notifyNewScreen('forgot_password');
|
||||||
break;
|
break;
|
||||||
case 'leave_room':
|
case 'leave_room':
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
|
|
||||||
var roomId = payload.room_id;
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Leave room",
|
title: "Leave room",
|
||||||
description: "Are you sure you want to leave the room?",
|
description: "Are you sure you want to leave the room?",
|
||||||
onFinished: function(should_leave) {
|
onFinished: (should_leave) => {
|
||||||
if (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 :(
|
// FIXME: controller shouldn't be loading a view :(
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
d.then(function() {
|
d.then(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
dis.dispatch({action: 'view_next_room'});
|
if (this.currentRoomId === payload.room_id) {
|
||||||
}, function(err) {
|
dis.dispatch({action: 'view_next_room'});
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
console.error("Failed to leave room " + payload.room_id + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to leave room",
|
title: "Failed to leave room",
|
||||||
|
description: "Server may be unavailable, overloaded, or you hit a bug."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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()
|
description: err.toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -509,8 +565,11 @@ module.exports = React.createClass({
|
|||||||
case 'set_theme':
|
case 'set_theme':
|
||||||
this._onSetTheme(payload.value);
|
this._onSetTheme(payload.value);
|
||||||
break;
|
break;
|
||||||
|
case 'on_logging_in':
|
||||||
|
this.setState({loggingIn: true});
|
||||||
|
break;
|
||||||
case 'on_logged_in':
|
case 'on_logged_in':
|
||||||
this._onLoggedIn();
|
this._onLoggedIn(payload.teamToken);
|
||||||
break;
|
break;
|
||||||
case 'on_logged_out':
|
case 'on_logged_out':
|
||||||
this._onLoggedOut();
|
this._onLoggedOut();
|
||||||
@ -582,36 +641,38 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sdkReady) {
|
// Wait for the first sync to complete so that if a room does have an alias,
|
||||||
// if the SDK is not ready yet, remember what room
|
// it would have been retrieved.
|
||||||
// we're supposed to be on but don't notify about
|
let waitFor = q(null);
|
||||||
// the new screen yet (we won't be showing it yet)
|
if (!this.firstSyncComplete) {
|
||||||
// The normal case where this happens is navigating
|
if (!this.firstSyncPromise) {
|
||||||
// to the room in the URL bar on page load.
|
console.warn('Cannot view a room before first sync. room_id:', room_info.room_id);
|
||||||
var presentedId = room_info.room_alias || room_info.room_id;
|
return;
|
||||||
var room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
}
|
||||||
|
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) {
|
if (room) {
|
||||||
var theAlias = Rooms.getDisplayAliasForRoom(room);
|
const theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||||
if (theAlias) presentedId = theAlias;
|
if (theAlias) presentedId = theAlias;
|
||||||
|
|
||||||
// No need to do this given RoomView triggers it itself...
|
// Store this as the ID of the last room accessed. This is so that we can
|
||||||
// var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
// persist which room is being stored across refreshes and browser quits.
|
||||||
// var color_scheme = {};
|
if (localStorage) {
|
||||||
// if (color_scheme_event) {
|
localStorage.setItem('mx_last_room_id', room.roomId);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room_info.event_id) {
|
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;
|
newState.ready = true;
|
||||||
}
|
this.setState(newState);
|
||||||
this.setState(newState);
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_createChat: function() {
|
_createChat: function() {
|
||||||
@ -637,6 +698,14 @@ module.exports = React.createClass({
|
|||||||
_onLoadCompleted: function() {
|
_onLoadCompleted: function() {
|
||||||
this.props.onLoadCompleted();
|
this.props.onLoadCompleted();
|
||||||
this.setState({loading: false});
|
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
|
* Called when a new logged in session has started
|
||||||
*/
|
*/
|
||||||
_onLoggedIn: function(credentials) {
|
_onLoggedIn: function(teamToken) {
|
||||||
this.guestCreds = null;
|
|
||||||
this.notifyNewScreen('');
|
|
||||||
this.setState({
|
this.setState({
|
||||||
screen: undefined,
|
guestCreds: null,
|
||||||
logged_in: true,
|
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() {
|
_onLoggedOut: function() {
|
||||||
this.notifyNewScreen('login');
|
this.notifyNewScreen('login');
|
||||||
this.setStateForNewScreen({
|
this.setStateForNewScreen({
|
||||||
logged_in: false,
|
loggedIn: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
@ -709,6 +813,7 @@ module.exports = React.createClass({
|
|||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
page_type: PageTypes.RoomDirectory,
|
page_type: PageTypes.RoomDirectory,
|
||||||
});
|
});
|
||||||
|
this._teamToken = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -716,9 +821,31 @@ module.exports = React.createClass({
|
|||||||
* (useful for setting listeners)
|
* (useful for setting listeners)
|
||||||
*/
|
*/
|
||||||
_onWillStartClient() {
|
_onWillStartClient() {
|
||||||
|
var self = this;
|
||||||
var cli = MatrixClientPeg.get();
|
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) {
|
cli.on('sync', function(state, prevState) {
|
||||||
self.updateStatusIndicator(state, prevState);
|
self.updateStatusIndicator(state, prevState);
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
@ -726,55 +853,12 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
console.log("MatrixClient sync state => %s", state);
|
console.log("MatrixClient sync state => %s", state);
|
||||||
if (state !== "PREPARED") { return; }
|
if (state !== "PREPARED") { return; }
|
||||||
self.sdkReady = true;
|
|
||||||
|
|
||||||
if (self.starting_room_alias_payload) {
|
self.firstSyncComplete = true;
|
||||||
dis.dispatch(self.starting_room_alias_payload);
|
self.firstSyncPromise.resolve();
|
||||||
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});
|
|
||||||
}
|
|
||||||
|
|
||||||
// we notifyNewScreen now because now the room will actually be displayed,
|
dis.dispatch({action: 'focus_composer'});
|
||||||
// and (mostly) now we can get the correct alias.
|
self.setState({ready: true});
|
||||||
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});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
cli.on('Call.incoming', function(call) {
|
cli.on('Call.incoming', function(call) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
@ -874,12 +958,7 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
// we can't view a room unless we're logged in
|
// we can't view a room unless we're logged in
|
||||||
// (a guest account is fine)
|
// (a guest account is fine)
|
||||||
if (!this.state.logged_in) {
|
if (this.state.loggedIn) {
|
||||||
// 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 {
|
|
||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
} else if (screen.indexOf('user/') == 0) {
|
} else if (screen.indexOf('user/') == 0) {
|
||||||
@ -973,29 +1052,17 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
onReturnToGuestClick: function() {
|
onReturnToGuestClick: function() {
|
||||||
// reanimate our guest login
|
// reanimate our guest login
|
||||||
if (this.guestCreds) {
|
if (this.state.guestCreds) {
|
||||||
Lifecycle.setLoggedIn(this.guestCreds);
|
Lifecycle.setLoggedIn(this.state.guestCreds);
|
||||||
this.guestCreds = null;
|
this.setState({guestCreds: null});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRegistered: function(credentials) {
|
onRegistered: function(credentials, teamToken) {
|
||||||
Lifecycle.setLoggedIn(credentials);
|
// teamToken may not be truthy
|
||||||
// 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) {
|
|
||||||
this._teamToken = teamToken;
|
this._teamToken = teamToken;
|
||||||
this._setPage(PageTypes.HomePage);
|
this._is_registered = true;
|
||||||
|
Lifecycle.setLoggedIn(credentials);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFinishPostRegistration: function() {
|
onFinishPostRegistration: function() {
|
||||||
@ -1061,15 +1128,20 @@ module.exports = React.createClass({
|
|||||||
this.setState({currentRoomId: room_id});
|
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() {
|
render: function() {
|
||||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
// `loading` might be set to false before `loggedIn = true`, causing the default
|
||||||
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
// (`<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
|
||||||
// console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
// actions `on_logging_in` and `on_logged_in`.
|
||||||
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
if (this.state.loading || this.state.loggingIn) {
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
if (this.state.loading) {
|
|
||||||
var Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@ -1078,15 +1150,17 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
// needs to be before normal PageTypes as you are logged in technically
|
// needs to be before normal PageTypes as you are logged in technically
|
||||||
else if (this.state.screen == 'post_registration') {
|
else if (this.state.screen == 'post_registration') {
|
||||||
|
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
||||||
return (
|
return (
|
||||||
<PostRegistration
|
<PostRegistration
|
||||||
onComplete={this.onFinishPostRegistration} />
|
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.
|
/* 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
|
* 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.
|
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||||
*/
|
*/
|
||||||
|
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||||
return (
|
return (
|
||||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||||
onRoomIdResolved={this.onRoomIdResolved}
|
onRoomIdResolved={this.onRoomIdResolved}
|
||||||
@ -1097,9 +1171,9 @@ module.exports = React.createClass({
|
|||||||
{...this.state}
|
{...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
|
// 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 (
|
return (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@ -1109,6 +1183,7 @@ module.exports = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.screen == 'register') {
|
} else if (this.state.screen == 'register') {
|
||||||
|
const Registration = sdk.getComponent('structures.login.Registration');
|
||||||
return (
|
return (
|
||||||
<Registration
|
<Registration
|
||||||
clientSecret={this.state.register_client_secret}
|
clientSecret={this.state.register_client_secret}
|
||||||
@ -1124,16 +1199,16 @@ module.exports = React.createClass({
|
|||||||
teamServerConfig={this.props.config.teamServerConfig}
|
teamServerConfig={this.props.config.teamServerConfig}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
registrationUrl={this.props.registrationUrl}
|
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||||
onTeamMemberRegistered={this.onTeamMemberRegistered}
|
|
||||||
onLoggedIn={this.onRegistered}
|
onLoggedIn={this.onRegistered}
|
||||||
onLoginClick={this.onLoginClick}
|
onLoginClick={this.onLoginClick}
|
||||||
onRegisterClick={this.onRegisterClick}
|
onRegisterClick={this.onRegisterClick}
|
||||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.state.screen == 'forgot_password') {
|
} else if (this.state.screen == 'forgot_password') {
|
||||||
|
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||||
return (
|
return (
|
||||||
<ForgotPassword
|
<ForgotPassword
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
defaultHsUrl={this.getDefaultHsUrl()}
|
||||||
@ -1145,7 +1220,8 @@ module.exports = React.createClass({
|
|||||||
onLoginClick={this.onLoginClick} />
|
onLoginClick={this.onLoginClick} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var r = (
|
const Login = sdk.getComponent('structures.login.Login');
|
||||||
|
return (
|
||||||
<Login
|
<Login
|
||||||
onLoggedIn={Lifecycle.setLoggedIn}
|
onLoggedIn={Lifecycle.setLoggedIn}
|
||||||
onRegisterClick={this.onRegisterClick}
|
onRegisterClick={this.onRegisterClick}
|
||||||
@ -1157,17 +1233,9 @@ module.exports = React.createClass({
|
|||||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||||
enableGuest={this.props.enableGuest}
|
enableGuest={this.props.enableGuest}
|
||||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||||
initialErrorText={this.sessionLoadError}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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);
|
var last = (i == lastShownEventIndex);
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
|
if (isMembershipChange(mxEv) &&
|
||||||
|
EventTile.haveTileForEvent(mxEv) &&
|
||||||
|
!mxEv.isRedacted()
|
||||||
|
) {
|
||||||
let ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
// member events. This will prevent it from being re-created unnecessarily, and
|
// member events. This will prevent it from being re-created unnecessarily, and
|
||||||
@ -349,7 +352,9 @@ module.exports = React.createClass({
|
|||||||
<MemberEventListSummary
|
<MemberEventListSummary
|
||||||
key={key}
|
key={key}
|
||||||
events={summarisedEvents}
|
events={summarisedEvents}
|
||||||
data-scroll-token={eventId}>
|
data-scroll-token={eventId}
|
||||||
|
onToggle={this._onWidgetLoad} // Update scroll state
|
||||||
|
>
|
||||||
{eventTiles}
|
{eventTiles}
|
||||||
</MemberEventListSummary>
|
</MemberEventListSummary>
|
||||||
);
|
);
|
||||||
@ -362,10 +367,6 @@ module.exports = React.createClass({
|
|||||||
// replacing all of the DOM elements every time we paginate.
|
// replacing all of the DOM elements every time we paginate.
|
||||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
|
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
|
||||||
prevEvent = mxEv;
|
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;
|
var isVisibleReadMarker = false;
|
||||||
@ -410,7 +411,9 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
var continuation = false;
|
var continuation = false;
|
||||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
|
||||||
|
if (prevEvent !== null
|
||||||
|
&& prevEvent.sender && mxEv.sender
|
||||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||||
&& mxEv.getType() == prevEvent.getType()) {
|
&& mxEv.getType() == prevEvent.getType()) {
|
||||||
continuation = true;
|
continuation = true;
|
||||||
@ -463,6 +466,7 @@ module.exports = React.createClass({
|
|||||||
ref={this._collectEventNode.bind(this, eventId)}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
data-scroll-token={scrollToken}>
|
data-scroll-token={scrollToken}>
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
|
isRedacted={mxEv.isRedacted()}
|
||||||
onWidgetLoad={this._onWidgetLoad}
|
onWidgetLoad={this._onWidgetLoad}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
readReceiptMap={this._readReceiptMap}
|
readReceiptMap={this._readReceiptMap}
|
||||||
@ -483,13 +487,17 @@ module.exports = React.createClass({
|
|||||||
// here.
|
// here.
|
||||||
return !this.props.suppressFirstDateSeparator;
|
return !this.props.suppressFirstDateSeparator;
|
||||||
}
|
}
|
||||||
|
const prevEventDate = prevEvent.getDate();
|
||||||
|
if (!nextEventDate || !prevEventDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Return early for events that are > 24h apart
|
// Return early for events that are > 24h apart
|
||||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare weekdays
|
// 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
|
// get a list of read receipts that should be shown next to this event
|
||||||
|
@ -96,26 +96,12 @@ module.exports = React.createClass({
|
|||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
|
|
||||||
|
this._checkSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function(prevProps, prevState) {
|
componentDidUpdate: function() {
|
||||||
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
this._checkSize();
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
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
|
// 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
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize: function(props, state) {
|
_getSize: function() {
|
||||||
if (state.syncState === "ERROR" ||
|
if (this.state.syncState === "ERROR" ||
|
||||||
(state.usersTyping.length > 0) ||
|
(this.state.usersTyping.length > 0) ||
|
||||||
props.numUnreadMessages ||
|
this.props.numUnreadMessages ||
|
||||||
!props.atEndOfLiveTimeline ||
|
!this.props.atEndOfLiveTimeline ||
|
||||||
props.hasActiveCall) {
|
this.props.hasActiveCall ||
|
||||||
|
this.props.tabComplete.isTabCompleting()
|
||||||
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (props.tabCompleteEntries) {
|
} else if (this.props.tabCompleteEntries) {
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
} else if (props.unsentMessageError) {
|
} else if (this.props.unsentMessageError) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
return STATUS_BAR_HIDDEN;
|
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.
|
// return suitable content for the image on the left of the status bar.
|
||||||
//
|
//
|
||||||
// if wantPlaceholder is true, we include a "..." placeholder if
|
// if wantPlaceholder is true, we include a "..." placeholder if
|
||||||
@ -194,8 +182,9 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasActiveCall) {
|
if (this.props.hasActiveCall) {
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
return (
|
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,
|
// called when state.room is first initialised (either at initial load,
|
||||||
// after a successful peek, or after we join the room).
|
// after a successful peek, or after we join the room).
|
||||||
_onRoomLoaded: function(room) {
|
_onRoomLoaded: function(room) {
|
||||||
|
this._warnAboutEncryption(room);
|
||||||
this._calculatePeekRules(room);
|
this._calculatePeekRules(room);
|
||||||
this._updatePreviewUrlVisibility(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) {
|
_calculatePeekRules: function(room) {
|
||||||
var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
|
var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
|
||||||
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
|
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
|
||||||
@ -716,17 +752,11 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onResendAllClick: function() {
|
onResendAllClick: function() {
|
||||||
var eventsToResend = this._getUnsentMessages(this.state.room);
|
Resend.resendUnsentEvents(this.state.room);
|
||||||
eventsToResend.forEach(function(event) {
|
|
||||||
Resend.resend(event);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancelAllClick: function() {
|
onCancelAllClick: function() {
|
||||||
var eventsToResend = this._getUnsentMessages(this.state.room);
|
Resend.cancelUnsentEvents(this.state.room);
|
||||||
eventsToResend.forEach(function(event) {
|
|
||||||
Resend.removeFromQueue(event);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onJoinButtonClicked: function(ev) {
|
onJoinButtonClicked: function(ev) {
|
||||||
@ -892,8 +922,6 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
uploadFile: function(file) {
|
uploadFile: function(file) {
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||||
Modal.createDialog(NeedToRegisterDialog, {
|
Modal.createDialog(NeedToRegisterDialog, {
|
||||||
@ -905,11 +933,20 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
ContentMessages.sendContentToRoom(
|
ContentMessages.sendContentToRoom(
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get()
|
file, this.state.room.roomId, MatrixClientPeg.get()
|
||||||
).done(undefined, function(error) {
|
).done(undefined, (error) => {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
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, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to upload file",
|
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) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Search failed: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Search failed",
|
title: "Search failed",
|
||||||
description: error.toString()
|
description: "Server may be unavailable, overloaded, or search timed out :("
|
||||||
});
|
});
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
@ -1639,14 +1677,14 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
videoMuteButton =
|
videoMuteButton =
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
<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"}
|
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
|
||||||
width="31" height="27"/>
|
width="31" height="27"/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
voiceMuteButton =
|
voiceMuteButton =
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
<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"}
|
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
|
||||||
width="21" height="26"/>
|
width="21" height="26"/>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
|||||||
|
|
||||||
// The amount of extra scroll distance to allow prior to unfilling.
|
// The amount of extra scroll distance to allow prior to unfilling.
|
||||||
// See _getExcessHeight.
|
// See _getExcessHeight.
|
||||||
const UNPAGINATION_PADDING = 3000;
|
const UNPAGINATION_PADDING = 6000;
|
||||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||||
// many scroll events causing many unfilling requests.
|
// many scroll events causing many unfilling requests.
|
||||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||||
@ -333,33 +333,27 @@ module.exports = React.createClass({
|
|||||||
if (excessHeight <= 0) {
|
if (excessHeight <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var itemlist = this.refs.itemlist;
|
const tiles = this.refs.itemlist.children;
|
||||||
var tiles = itemlist.children;
|
|
||||||
|
|
||||||
// The scroll token of the first/last tile to be unpaginated
|
// The scroll token of the first/last tile to be unpaginated
|
||||||
let markerScrollToken = null;
|
let markerScrollToken = null;
|
||||||
|
|
||||||
// Subtract clientHeights to simulate the events being unpaginated whilst counting
|
// Subtract heights of tiles to simulate the tiles being unpaginated until the
|
||||||
// the events to be unpaginated.
|
// excess height is less than the height of the next tile to subtract. This
|
||||||
if (backwards) {
|
// prevents excessHeight becoming negative, which could lead to future
|
||||||
// Iterate forwards from start of tiles, subtracting event tile height
|
// pagination.
|
||||||
let i = 0;
|
//
|
||||||
while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
|
// If backwards is true, we unpaginate (remove) tiles from the back (top).
|
||||||
excessHeight -= tiles[i].clientHeight;
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
if (tiles[i].dataset.scrollToken) {
|
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
// Subtract height of tile as if it were unpaginated
|
||||||
}
|
excessHeight -= tile.clientHeight;
|
||||||
i++;
|
// The tile may not have a scroll token, so guard it
|
||||||
|
if (tile.dataset.scrollToken) {
|
||||||
|
markerScrollToken = tile.dataset.scrollToken;
|
||||||
}
|
}
|
||||||
} else {
|
if (tile.clientHeight > excessHeight) {
|
||||||
// Iterate backwards from end of tiles, subtracting event tile height
|
break;
|
||||||
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--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,24 +583,34 @@ module.exports = React.createClass({
|
|||||||
var itemlist = this.refs.itemlist;
|
var itemlist = this.refs.itemlist;
|
||||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||||
var messages = itemlist.children;
|
var messages = itemlist.children;
|
||||||
|
let newScrollState = null;
|
||||||
|
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (var i = messages.length-1; i >= 0; --i) {
|
||||||
var node = messages[i];
|
var node = messages[i];
|
||||||
if (!node.dataset.scrollToken) continue;
|
if (!node.dataset.scrollToken) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
newScrollState = {
|
||||||
this.scrollState = {
|
stuckAtBottom: false,
|
||||||
stuckAtBottom: false,
|
trackedScrollToken: node.dataset.scrollToken,
|
||||||
trackedScrollToken: node.dataset.scrollToken,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
};
|
||||||
};
|
// If the bottom of the panel intersects the ClientRect of node, use this node
|
||||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
// as the scrollToken.
|
||||||
return;
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This is only false if there were no nodes with `node.dataset.scrollToken` set.
|
||||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
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() {
|
_restoreSavedScrollState: function() {
|
||||||
|
@ -251,10 +251,12 @@ var TimelinePanel = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||||
|
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
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 eventId = scrollToken;
|
||||||
|
|
||||||
let marker = this.state.events.findIndex(
|
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) {
|
onRoomRedaction: function(ev, room) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
@ -469,14 +475,6 @@ var TimelinePanel = React.createClass({
|
|||||||
// we still have a client.
|
// we still have a client.
|
||||||
if (!MatrixClientPeg.get()) return;
|
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 currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||||
|
|
||||||
@ -514,6 +512,19 @@ var TimelinePanel = React.createClass({
|
|||||||
// it failed, so allow retries next time the user is active
|
// it failed, so allow retries next time the user is active
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// go via the dispatcher so that the URL is updated
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
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() {
|
componentDidMount: function() {
|
||||||
dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -39,6 +40,10 @@ const REACT_SDK_VERSION =
|
|||||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||||
// 'label' is how we describe it in the UI.
|
// 'label' is how we describe it in the UI.
|
||||||
const SETTINGS_LABELS = [
|
const SETTINGS_LABELS = [
|
||||||
|
{
|
||||||
|
id: 'autoplayGifsAndVideos',
|
||||||
|
label: 'Autoplay GIFs and videos',
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
id: 'alwaysShowTimestamps',
|
id: 'alwaysShowTimestamps',
|
||||||
@ -135,6 +140,7 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
this._addThreepid = null;
|
||||||
|
|
||||||
if (PlatformPeg.get()) {
|
if (PlatformPeg.get()) {
|
||||||
q().then(() => {
|
q().then(() => {
|
||||||
@ -202,9 +208,10 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to load user settings: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Can't load user settings",
|
title: "Can't load user settings",
|
||||||
description: error.toString()
|
description: "Server may be unavailable or overloaded",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -242,10 +249,11 @@ module.exports = React.createClass({
|
|||||||
self._refreshFromServer();
|
self._refreshFromServer();
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
|
console.error("Failed to set avatar: " + err);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to set avatar. " + errMsg
|
description: "Failed to set avatar."
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -256,12 +264,18 @@ module.exports = React.createClass({
|
|||||||
title: "Sign out?",
|
title: "Sign out?",
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
For security, logging out will delete any end-to-end encryption keys from this browser,
|
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>,
|
If you want to be able to decrypt your conversation history from future Riot sessions,
|
||||||
but for now be warned.
|
please export your room keys for safe-keeping.
|
||||||
</div>,
|
</div>,
|
||||||
button: "Sign out",
|
button: "Sign out",
|
||||||
|
extraButtons: [
|
||||||
|
<button className="mx_Dialog_primary"
|
||||||
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
|
Export E2E room keys
|
||||||
|
</button>
|
||||||
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
dis.dispatch({action: 'logout'});
|
dis.dispatch({action: 'logout'});
|
||||||
@ -282,6 +296,7 @@ module.exports = React.createClass({
|
|||||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to change password: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: errMsg
|
description: errMsg
|
||||||
@ -308,12 +323,16 @@ module.exports = React.createClass({
|
|||||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAddThreepidClicked: function(value, shouldSubmit) {
|
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||||
if (!shouldSubmit) return;
|
if (!shouldSubmit) return;
|
||||||
|
this._addEmail();
|
||||||
|
},
|
||||||
|
|
||||||
|
_addEmail: function() {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
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)) {
|
if (!Email.looksValid(email_address)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Invalid Email Address",
|
title: "Invalid Email Address",
|
||||||
@ -321,10 +340,10 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.add_threepid = new AddThreepid();
|
this._addThreepid = new AddThreepid();
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this.add_threepid.addEmailAddress(email_address, true).done(() => {
|
this._addThreepid.addEmailAddress(email_address, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Verification Pending",
|
title: "Verification Pending",
|
||||||
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||||
@ -333,12 +352,13 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
|
console.error("Unable to add email address " + email_address + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to add email address",
|
title: "Error",
|
||||||
description: err.message
|
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});
|
this.setState({email_add_pending: true});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -357,9 +377,10 @@ module.exports = React.createClass({
|
|||||||
return this._refreshFromServer();
|
return this._refreshFromServer();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to remove contact information",
|
title: "Error",
|
||||||
description: err.toString(),
|
description: "Unable to remove contact information",
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
@ -376,8 +397,8 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
verifyEmailAddress: function() {
|
verifyEmailAddress: function() {
|
||||||
this.add_threepid.checkEmailLinkClicked().done(() => {
|
this._addThreepid.checkEmailLinkClicked().done(() => {
|
||||||
this.add_threepid = undefined;
|
this._addThreepid = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: "UserSettings.LOADING",
|
phase: "UserSettings.LOADING",
|
||||||
});
|
});
|
||||||
@ -397,9 +418,10 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to verify email address",
|
title: "Error",
|
||||||
description: err.toString(),
|
description: "Unable to verify email address",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -419,10 +441,11 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_onClearCacheClicked: function() {
|
_onClearCacheClicked: function() {
|
||||||
|
if (!PlatformPeg.get()) return;
|
||||||
|
|
||||||
|
MatrixClientPeg.get().stopClient();
|
||||||
MatrixClientPeg.get().store.deleteAllData().done(() => {
|
MatrixClientPeg.get().store.deleteAllData().done(() => {
|
||||||
// forceReload=false since we don't really need new HTML/JS files
|
PlatformPeg.get().reload();
|
||||||
// we just need to restart the JS runtime.
|
|
||||||
window.location.reload(false);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -745,6 +768,14 @@ module.exports = React.createClass({
|
|||||||
return medium[0].toUpperCase() + medium.slice(1);
|
return medium[0].toUpperCase() + medium.slice(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
presentableTextForThreepid: function(threepid) {
|
||||||
|
if (threepid.medium == 'msisdn') {
|
||||||
|
return '+' + threepid.address;
|
||||||
|
} else {
|
||||||
|
return threepid.address;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
@ -777,7 +808,9 @@ module.exports = React.createClass({
|
|||||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<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>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<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)} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
var addThreepidSection;
|
let addEmailSection;
|
||||||
if (this.state.email_add_pending) {
|
if (this.state.email_add_pending) {
|
||||||
addThreepidSection = <Loader />;
|
addEmailSection = <Loader key="_email_add_spinner" />;
|
||||||
} else if (!MatrixClientPeg.get().isGuest()) {
|
} else if (!MatrixClientPeg.get().isGuest()) {
|
||||||
addThreepidSection = (
|
addEmailSection = (
|
||||||
<div className="mx_UserSettings_profileTableRow" key="new">
|
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<EditableText
|
<EditableText
|
||||||
ref="add_threepid_input"
|
ref="add_email_input"
|
||||||
className="mx_UserSettings_editable"
|
className="mx_UserSettings_editable"
|
||||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||||
placeholder={ "Add email address" }
|
placeholder={ "Add email address" }
|
||||||
blurToCancel={ false }
|
blurToCancel={ false }
|
||||||
onValueChanged={ this.onAddThreepidClicked } />
|
onValueChanged={ this._onAddEmailEditFinished } />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<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>
|
||||||
</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;
|
var accountJsx;
|
||||||
|
|
||||||
|
@ -93,11 +93,17 @@ module.exports = React.createClass({
|
|||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||||
making encrypted chat history unreadable.
|
making encrypted chat history unreadable, unless you first export your room keys
|
||||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
and re-import them afterwards.
|
||||||
but for now be warned.
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||||
</div>,
|
</div>,
|
||||||
button: "Continue",
|
button: "Continue",
|
||||||
|
extraButtons: [
|
||||||
|
<button className="mx_Dialog_primary"
|
||||||
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
|
Export E2E room keys
|
||||||
|
</button>
|
||||||
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.submitPasswordReset(
|
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) {
|
onInputChanged: function(stateKey, ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
[stateKey]: ev.target.value
|
[stateKey]: ev.target.value
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -19,13 +20,13 @@ limitations under the License.
|
|||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
var ReactDOM = require('react-dom');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var Signup = require("../../../Signup");
|
var Login = require("../../../Login");
|
||||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||||
var CasLogin = require("../../views/login/CasLogin");
|
var CasLogin = require("../../views/login/CasLogin");
|
||||||
var ServerConfig = require("../../views/login/ServerConfig");
|
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({
|
module.exports = React.createClass({
|
||||||
displayName: 'Login',
|
displayName: 'Login',
|
||||||
@ -52,20 +53,20 @@ module.exports = React.createClass({
|
|||||||
// login shouldn't care how password recovery is done.
|
// login shouldn't care how password recovery is done.
|
||||||
onForgotPasswordClick: React.PropTypes.func,
|
onForgotPasswordClick: React.PropTypes.func,
|
||||||
onCancelClick: React.PropTypes.func,
|
onCancelClick: React.PropTypes.func,
|
||||||
|
|
||||||
initialErrorText: React.PropTypes.string,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
errorText: this.props.initialErrorText,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||||
|
|
||||||
// used for preserving username when changing homeserver
|
// used for preserving form values when changing homeserver
|
||||||
username: "",
|
username: "",
|
||||||
|
phoneCountry: null,
|
||||||
|
phoneNumber: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -73,20 +74,21 @@ module.exports = React.createClass({
|
|||||||
this._initLoginLogic();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
var self = this;
|
this.setState({
|
||||||
self.setState({
|
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
this._loginLogic.loginViaPassword(
|
||||||
self.props.onLoggedIn(data);
|
username, phoneCountry, phoneNumber, password,
|
||||||
}, function(error) {
|
).then((data) => {
|
||||||
self._setStateFromError(error, true);
|
this.props.onLoggedIn(data);
|
||||||
}).finally(function() {
|
}, (error) => {
|
||||||
self.setState({
|
this._setStateFromError(error, true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
busy: false
|
busy: false
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
@ -119,6 +121,14 @@ module.exports = React.createClass({
|
|||||||
this.setState({ username: username });
|
this.setState({ username: username });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPhoneCountryChanged: function(phoneCountry) {
|
||||||
|
this.setState({ phoneCountry: phoneCountry });
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(phoneNumber) {
|
||||||
|
this.setState({ phoneNumber: phoneNumber });
|
||||||
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -146,7 +156,7 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||||
|
|
||||||
var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, {
|
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
});
|
});
|
||||||
this._loginLogic = loginLogic;
|
this._loginLogic = loginLogic;
|
||||||
@ -225,7 +235,11 @@ module.exports = React.createClass({
|
|||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
initialUsername={this.state.username}
|
initialUsername={this.state.username}
|
||||||
|
initialPhoneCountry={this.state.phoneCountry}
|
||||||
|
initialPhoneNumber={this.state.phoneNumber}
|
||||||
onUsernameChanged={this.onUsernameChanged}
|
onUsernameChanged={this.onUsernameChanged}
|
||||||
|
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||||
|
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,25 +15,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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');
|
import sdk from '../../../index';
|
||||||
var dis = require('../../../dispatcher');
|
import dis from '../../../dispatcher';
|
||||||
var Signup = require("../../../Signup");
|
import ServerConfig from '../../views/login/ServerConfig';
|
||||||
var ServerConfig = require("../../views/login/ServerConfig");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
import CaptchaForm from '../../views/login/CaptchaForm';
|
||||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
import RtsClient from '../../../RtsClient';
|
||||||
var RtsClient = require("../../../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({
|
module.exports = React.createClass({
|
||||||
displayName: 'Registration',
|
displayName: 'Registration',
|
||||||
|
|
||||||
@ -40,7 +37,7 @@ module.exports = React.createClass({
|
|||||||
onLoggedIn: React.PropTypes.func.isRequired,
|
onLoggedIn: React.PropTypes.func.isRequired,
|
||||||
clientSecret: React.PropTypes.string,
|
clientSecret: React.PropTypes.string,
|
||||||
sessionId: React.PropTypes.string,
|
sessionId: React.PropTypes.string,
|
||||||
registrationUrl: React.PropTypes.string,
|
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||||
idSid: React.PropTypes.string,
|
idSid: React.PropTypes.string,
|
||||||
customHsUrl: React.PropTypes.string,
|
customHsUrl: React.PropTypes.string,
|
||||||
customIsUrl: React.PropTypes.string,
|
customIsUrl: React.PropTypes.string,
|
||||||
@ -58,7 +55,6 @@ module.exports = React.createClass({
|
|||||||
teamServerURL: React.PropTypes.string.isRequired,
|
teamServerURL: React.PropTypes.string.isRequired,
|
||||||
}),
|
}),
|
||||||
teamSelected: React.PropTypes.object,
|
teamSelected: React.PropTypes.object,
|
||||||
onTeamMemberRegistered: React.PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
@ -82,27 +78,20 @@ module.exports = React.createClass({
|
|||||||
formVals: {
|
formVals: {
|
||||||
email: this.props.email,
|
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() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
// attach this to the instance rather than this.state since it isn't UI
|
this._replaceClient();
|
||||||
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();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.props.teamServerConfig &&
|
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) {
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
this.registerLogic.setHomeserverUrl(newHsUrl);
|
this.setState({
|
||||||
|
hsUrl: newHsUrl,
|
||||||
|
});
|
||||||
|
this._replaceClient();
|
||||||
},
|
},
|
||||||
|
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
onIsUrlChanged: function(newIsUrl) {
|
||||||
this.registerLogic.setIdentityServerUrl(newIsUrl);
|
this.setState({
|
||||||
|
isUrl: newIsUrl,
|
||||||
|
});
|
||||||
|
this._replaceClient();
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
_replaceClient: function() {
|
||||||
if (payload.action !== "registration_step_update") {
|
this._matrixClient = Matrix.createClient({
|
||||||
return;
|
baseUrl: this.state.hsUrl,
|
||||||
}
|
idBaseUrl: this.state.isUrl,
|
||||||
// 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
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onFormSubmit: function(formVals) {
|
onFormSubmit: function(formVals) {
|
||||||
var self = this;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: "",
|
errorText: "",
|
||||||
busy: true,
|
busy: true,
|
||||||
formVals: formVals,
|
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
|
_onUIAuthFinished: function(success, response, extra) {
|
||||||
onProcessingRegistration: function(promise) {
|
if (!success) {
|
||||||
var self = this;
|
let msg = response.message || response.toString();
|
||||||
promise.done(function(response) {
|
// can we give a better error message?
|
||||||
self.setState({
|
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
busy: false
|
let msisdn_available = false;
|
||||||
});
|
for (const flow of response.available_flows) {
|
||||||
if (!response || !response.access_token) {
|
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||||
console.warn(
|
}
|
||||||
"FIXME: Register fulfilled without a final response, " +
|
if (!msisdn_available) {
|
||||||
"did you break the promise chain?"
|
msg = "This server does not support authentication with a phone number";
|
||||||
);
|
}
|
||||||
// no matter, we'll grab it direct
|
|
||||||
response = self.registerLogic.getCredentials();
|
|
||||||
}
|
}
|
||||||
if (!response || !response.user_id || !response.access_token) {
|
this.setState({
|
||||||
console.error("Final response is missing keys.");
|
busy: false,
|
||||||
self.setState({
|
doingUIAuth: false,
|
||||||
errorText: "Registration failed on server"
|
errorText: msg,
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.props.onLoggedIn({
|
|
||||||
userId: response.user_id,
|
|
||||||
deviceId: response.device_id,
|
|
||||||
homeserverUrl: self.registerLogic.getHomeserverUrl(),
|
|
||||||
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
|
||||||
accessToken: response.access_token
|
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Done regardless of `teamSelected`. People registering with non-team emails
|
this.setState({
|
||||||
// will just nop. The point of this being we might not have the email address
|
// we're still busy until we get unmounted: don't show the registration form again
|
||||||
// that the user registered with at this stage (depending on whether this
|
busy: true,
|
||||||
// is the client they initiated registration).
|
doingUIAuth: false,
|
||||||
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);
|
|
||||||
|
|
||||||
self._rtsClient.getTeam(teamToken).then((team) => {
|
// Done regardless of `teamSelected`. People registering with non-team emails
|
||||||
console.log(
|
// will just nop. The point of this being we might not have the email address
|
||||||
`User successfully registered with team ${team.name}`
|
// that the user registered with at this stage (depending on whether this
|
||||||
);
|
// is the client they initiated registration).
|
||||||
if (!team.rooms) {
|
let trackPromise = q(null);
|
||||||
return;
|
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) => {
|
}, (err) => {
|
||||||
console.error('Error tracking referral', err);
|
console.error('Error getting team config', err);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (self.props.brand) {
|
return teamToken;
|
||||||
MatrixClientPeg.get().getPushers().done((resp)=>{
|
}, (err) => {
|
||||||
var pushers = resp.pushers;
|
console.error('Error tracking referral', err);
|
||||||
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
|
|
||||||
});
|
});
|
||||||
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":
|
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||||
errMsg = "This doesn't look like a valid email address";
|
errMsg = "This doesn't look like a valid email address";
|
||||||
break;
|
break;
|
||||||
|
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||||
|
errMsg = "This doesn't look like a valid phone number";
|
||||||
|
break;
|
||||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||||
break;
|
break;
|
||||||
@ -316,116 +291,121 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onCaptchaResponse: function(response) {
|
|
||||||
this.registerLogic.tellStage("m.login.recaptcha", {
|
|
||||||
response: response
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onTeamSelected: function(teamSelected) {
|
onTeamSelected: function(teamSelected) {
|
||||||
if (!this._unmounted) {
|
if (!this._unmounted) {
|
||||||
this.setState({ teamSelected });
|
this.setState({ teamSelected });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_getRegisterContentJsx: function() {
|
_makeRegisterRequest: function(auth) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
let guestAccessToken = this.props.guestAccessToken;
|
||||||
|
|
||||||
var currStep = this.registerLogic.getStep();
|
if (
|
||||||
var registerStep;
|
this.state.formVals.username !== this.props.username ||
|
||||||
switch (currStep) {
|
this.state.hsUrl != this.props.defaultHsUrl
|
||||||
case "Register.COMPLETE":
|
) {
|
||||||
break; // NOP
|
// don't try to upgrade if we changed our username
|
||||||
case "Register.START":
|
// or are registering on a different HS
|
||||||
case "Register.STEP_m.login.dummy":
|
guestAccessToken = null;
|
||||||
// NB. Our 'username' prop is specifically for upgrading
|
}
|
||||||
// a guest account
|
|
||||||
if (this.state.teamServerBusy) {
|
// Only send the bind params if we're sending username / pw params
|
||||||
registerStep = <Spinner />;
|
// (Since we need to send no params at all to use the ones saved in the
|
||||||
break;
|
// session).
|
||||||
}
|
const bindThreepids = this.state.formVals.password ? {
|
||||||
registerStep = (
|
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
|
<RegistrationForm
|
||||||
showEmail={true}
|
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||||
|
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.state.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={this.props.username}
|
guestUsername={guestUsername}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
onTeamSelected={this.onTeamSelected}
|
onTeamSelected={this.onTeamSelected}
|
||||||
/>
|
/>
|
||||||
);
|
{errorSection}
|
||||||
break;
|
<ServerConfig ref="serverConfig"
|
||||||
case "Register.STEP_m.login.email.identity":
|
withToggleButton={true}
|
||||||
registerStep = (
|
customHsUrl={this.props.customHsUrl}
|
||||||
<div>
|
customIsUrl={this.props.customIsUrl}
|
||||||
Please check your email to continue registration.
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
</div>
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
);
|
onHsUrlChanged={this.onHsUrlChanged}
|
||||||
break;
|
onIsUrlChanged={this.onIsUrlChanged}
|
||||||
case "Register.STEP_m.login.recaptcha":
|
delayTimeMs={1000}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
);
|
</div>
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error("Unknown register state: %s", currStep);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
var busySpinner;
|
|
||||||
if (this.state.busy) {
|
|
||||||
busySpinner = (
|
|
||||||
<Spinner />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnToAppJsx;
|
let returnToAppJsx;
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
returnToAppJsx =
|
returnToAppJsx = (
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
Return to app
|
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>
|
</a>
|
||||||
{ returnToAppJsx }
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
|
||||||
var LoginFooter = sdk.getComponent('login.LoginFooter');
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
@ -435,7 +415,12 @@ module.exports = React.createClass({
|
|||||||
this.state.teamSelected.domain + "/icon.png" :
|
this.state.teamSelected.domain + "/icon.png" :
|
||||||
null}
|
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 />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import * as KeyCode from '../../../KeyCode';
|
import * as KeyCode from '../../../KeyCode';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic container for modal dialogs.
|
* Basic container for modal dialogs.
|
||||||
@ -65,15 +66,14 @@ export default React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||||
<AccessibleButton onClick={this._onCancelClick}
|
<AccessibleButton onClick={this._onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
>
|
>
|
||||||
<img
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
src="img/cancel.svg" width="18" height="18"
|
|
||||||
alt="Cancel" title="Cancel"
|
|
||||||
/>
|
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div className='mx_Dialog_title'>
|
<div className='mx_Dialog_title'>
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
|
115
src/components/views/dialogs/ChatCreateOrReuseDialog.js
Normal file
115
src/components/views/dialogs/ChatCreateOrReuseDialog.js
Normal file
@ -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 Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
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({
|
module.exports = React.createClass({
|
||||||
displayName: "ChatInviteDialog",
|
displayName: "ChatInviteDialog",
|
||||||
propTypes: {
|
propTypes: {
|
||||||
@ -85,6 +77,19 @@ module.exports = React.createClass({
|
|||||||
// Set the cursor at the end of the text input
|
// Set the cursor at the end of the text input
|
||||||
this.refs.textinput.value = this.props.value;
|
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();
|
this._updateUserList();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -97,18 +102,27 @@ module.exports = React.createClass({
|
|||||||
if (inviteList === null) return;
|
if (inviteList === null) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addrTexts = inviteList.map(addr => addr.address);
|
||||||
if (inviteList.length > 0) {
|
if (inviteList.length > 0) {
|
||||||
if (this._isDmChat(inviteList)) {
|
if (this._isDmChat(addrTexts)) {
|
||||||
|
const userId = inviteList[0].address;
|
||||||
// Direct Message chat
|
// Direct Message chat
|
||||||
var room = this._getDirectMessageRoom(inviteList[0]);
|
const rooms = this._getDirectMessageRooms(userId);
|
||||||
if (room) {
|
if (rooms.length > 0) {
|
||||||
// A Direct Message room already exists for this user and you
|
// A Direct Message room already exists for this user, so select a
|
||||||
// so go straight to that room
|
// room from a list that is similar to the one in MemberInfo panel
|
||||||
dis.dispatch({
|
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||||
action: 'view_room',
|
"views.dialogs.ChatCreateOrReuseDialog"
|
||||||
room_id: room.roomId,
|
);
|
||||||
|
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 {
|
} else {
|
||||||
this._startChat(inviteList);
|
this._startChat(inviteList);
|
||||||
}
|
}
|
||||||
@ -167,45 +181,59 @@ module.exports = React.createClass({
|
|||||||
const query = ev.target.value;
|
const query = ev.target.value;
|
||||||
let queryList = [];
|
let queryList = [];
|
||||||
|
|
||||||
// Only do search if there is something to search
|
if (query.length < 2) {
|
||||||
if (query.length > 0 && query != '@') {
|
return;
|
||||||
// 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 the query isn't a user we know about, but is a
|
if (this.queryChangedDebouncer) {
|
||||||
// valid address, add an entry for that
|
clearTimeout(this.queryChangedDebouncer);
|
||||||
if (queryList.length == 0) {
|
}
|
||||||
|
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);
|
const addrType = getAddressType(query);
|
||||||
if (addrType !== null) {
|
if (addrType !== null) {
|
||||||
queryList[0] = {
|
queryList.unshift({
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: query,
|
address: query,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
};
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
if (addrType == 'email') {
|
if (addrType == 'email') {
|
||||||
this._lookupThreepid(addrType, query).done();
|
this._lookupThreepid(addrType, query).done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
this.setState({
|
||||||
|
queryList: queryList,
|
||||||
this.setState({
|
error: false,
|
||||||
queryList: queryList,
|
}, () => {
|
||||||
error: false,
|
this.addressSelector.moveSelectionTop();
|
||||||
});
|
});
|
||||||
|
}, 200);
|
||||||
},
|
},
|
||||||
|
|
||||||
onDismissed: function(index) {
|
onDismissed: function(index) {
|
||||||
@ -238,22 +266,20 @@ module.exports = React.createClass({
|
|||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getDirectMessageRoom: function(addr) {
|
_getDirectMessageRooms: function(addr) {
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
var dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||||
if (dmRooms.length > 0) {
|
const rooms = [];
|
||||||
// Cycle through all the DM rooms and find the first non forgotten or parted room
|
dmRooms.forEach(dmRoom => {
|
||||||
for (let i = 0; i < dmRooms.length; i++) {
|
let room = MatrixClientPeg.get().getRoom(dmRoom);
|
||||||
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
|
if (room) {
|
||||||
if (room) {
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
if (me.membership == 'join') {
|
||||||
if (me.membership == 'join') {
|
rooms.push(room);
|
||||||
return room;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
return null;
|
return rooms;
|
||||||
},
|
},
|
||||||
|
|
||||||
_startChat: function(addrs) {
|
_startChat: function(addrs) {
|
||||||
@ -282,8 +308,8 @@ module.exports = React.createClass({
|
|||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failure to invite",
|
title: "Error",
|
||||||
description: err.toString()
|
description: "Failed to invite",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -295,8 +321,8 @@ module.exports = React.createClass({
|
|||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failure to invite user",
|
title: "Error",
|
||||||
description: err.toString()
|
description: "Failed to invite user",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -316,8 +342,8 @@ module.exports = React.createClass({
|
|||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failure to invite",
|
title: "Error",
|
||||||
description: err.toString()
|
description: "Failed to invite",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -331,49 +357,15 @@ module.exports = React.createClass({
|
|||||||
_updateUserList: new rate_limited_func(function() {
|
_updateUserList: new rate_limited_func(function() {
|
||||||
// Get all the users
|
// Get all the users
|
||||||
this._userList = MatrixClientPeg.get().getUsers();
|
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),
|
}, 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) {
|
_isOnInviteList: function(uid) {
|
||||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||||
if (
|
if (
|
||||||
@ -386,8 +378,11 @@ module.exports = React.createClass({
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_isDmChat: function(addrs) {
|
_isDmChat: function(addrTexts) {
|
||||||
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
if (addrTexts.length === 1 &&
|
||||||
|
getAddressType(addrTexts[0]) === "mx" &&
|
||||||
|
!this.props.roomId
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
73
src/components/views/dialogs/ConfirmRedactDialog.js
Normal file
73
src/components/views/dialogs/ConfirmRedactDialog.js
Normal file
@ -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_Dialog_content">
|
||||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
<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>
|
||||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||||
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import Velocity from 'velocity-vector';
|
import Velocity from 'velocity-vector';
|
||||||
|
|
||||||
export default class DeactivateAccountDialog extends React.Component {
|
export default class DeactivateAccountDialog extends React.Component {
|
||||||
|
@ -27,6 +27,9 @@ export default React.createClass({
|
|||||||
displayName: 'InteractiveAuthDialog',
|
displayName: 'InteractiveAuthDialog',
|
||||||
|
|
||||||
propTypes: {
|
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
|
// response from initial request. If not supplied, will do a request on
|
||||||
// mount.
|
// mount.
|
||||||
authData: React.PropTypes.shape({
|
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() {
|
render: function() {
|
||||||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
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 (
|
return (
|
||||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this.props.title}
|
title={this.state.authError ? 'Error' : this.props.title}
|
||||||
>
|
>
|
||||||
<div>
|
{content}
|
||||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
|
||||||
authData={this.props.authData}
|
|
||||||
makeRequest={this.props.makeRequest}
|
|
||||||
onFinished={this.props.onFinished}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -21,10 +21,8 @@ export default React.createClass({
|
|||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
description: React.PropTypes.oneOfType([
|
description: React.PropTypes.node,
|
||||||
React.PropTypes.element,
|
extraButtons: React.PropTypes.node,
|
||||||
React.PropTypes.string,
|
|
||||||
]),
|
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
focus: React.PropTypes.bool,
|
focus: React.PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
@ -34,8 +32,10 @@ export default React.createClass({
|
|||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
extraButtons: null,
|
||||||
button: "OK",
|
button: "OK",
|
||||||
focus: true,
|
focus: true,
|
||||||
|
hasCancelButton: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -49,6 +49,11 @@ export default React.createClass({
|
|||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const cancelButton = this.props.hasCancelButton ? (
|
||||||
|
<button onClick={this.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={ this.onOk }
|
||||||
@ -61,10 +66,8 @@ export default React.createClass({
|
|||||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
|
{this.props.extraButtons}
|
||||||
<button onClick={this.onCancel}>
|
{cancelButton}
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
@ -16,8 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
import Resend from '../../../Resend';
|
||||||
|
|
||||||
function DeviceListEntry(props) {
|
function DeviceListEntry(props) {
|
||||||
const {userId, device} = props;
|
const {userId, device} = props;
|
||||||
@ -85,7 +87,7 @@ UnknownDeviceList.propTypes = {
|
|||||||
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'UnknownEventDialog',
|
displayName: 'UnknownDeviceDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
@ -125,14 +127,10 @@ export default React.createClass({
|
|||||||
} else {
|
} else {
|
||||||
warning = (
|
warning = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
|
||||||
This means there is no guarantee that the devices
|
|
||||||
belong to the users they claim to.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
We recommend you go through the verification process
|
We recommend you go through the verification process
|
||||||
for each device before continuing, but you can resend
|
for each device to confirm they belong to their legitimate owner,
|
||||||
the message without verifying if you prefer.
|
but you can resend the message without verifying if you prefer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -151,8 +149,7 @@ export default React.createClass({
|
|||||||
>
|
>
|
||||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||||
<h4>
|
<h4>
|
||||||
This room contains unknown devices which have not been
|
This room contains devices that you haven't seen before.
|
||||||
verified.
|
|
||||||
</h4>
|
</h4>
|
||||||
{ warning }
|
{ warning }
|
||||||
Unknown devices:
|
Unknown devices:
|
||||||
@ -160,6 +157,13 @@ export default React.createClass({
|
|||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<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 }
|
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
|
@ -27,8 +27,8 @@ import React from 'react';
|
|||||||
export default function AccessibleButton(props) {
|
export default function AccessibleButton(props) {
|
||||||
const {element, onClick, children, ...restProps} = props;
|
const {element, onClick, children, ...restProps} = props;
|
||||||
restProps.onClick = onClick;
|
restProps.onClick = onClick;
|
||||||
restProps.onKeyDown = function(e) {
|
restProps.onKeyUp = function(e) {
|
||||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||||
};
|
};
|
||||||
restProps.tabIndex = restProps.tabIndex || "0";
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
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() {
|
moveSelectionUp: function() {
|
||||||
if (this.state.selected > 0) {
|
if (this.state.selected > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -124,7 +133,14 @@ export default React.createClass({
|
|||||||
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
||||||
// method, how far to scroll when using the arrow keys
|
// method, how far to scroll when using the arrow keys
|
||||||
addressList.push(
|
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" />
|
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -64,19 +64,14 @@ export default React.createClass({
|
|||||||
const address = this.props.address;
|
const address = this.props.address;
|
||||||
const name = address.displayName || address.address;
|
const name = address.displayName || address.address;
|
||||||
|
|
||||||
let imgUrl;
|
let imgUrls = [];
|
||||||
if (address.avatarMxc) {
|
|
||||||
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
|
||||||
address.avatarMxc, 25, 25, 'crop'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.addressType === "mx") {
|
if (address.addressType === "mx" && address.avatarMxc) {
|
||||||
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
address.avatarMxc, 25, 25, 'crop'
|
||||||
|
));
|
||||||
} else if (address.addressType === 'email') {
|
} else if (address.addressType === 'email') {
|
||||||
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
imgUrls.push('img/icon-email-user.svg');
|
||||||
} else {
|
|
||||||
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removing networks for now as they're not really supported
|
// Removing networks for now as they're not really supported
|
||||||
@ -168,7 +163,7 @@ export default React.createClass({
|
|||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_AddressTile_avatar">
|
<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>
|
</div>
|
||||||
{ info }
|
{ info }
|
||||||
{ dismiss }
|
{ dismiss }
|
||||||
|
@ -59,7 +59,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onKeyUp(ev) {
|
_onKeyUp(ev) {
|
||||||
if (ev.key == 'Enter') {
|
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||||
if (this.props.onJoinClick) {
|
if (this.props.onJoinClick) {
|
||||||
this.props.onJoinClick(this.state.value);
|
this.props.onJoinClick(this.state.value);
|
||||||
}
|
}
|
||||||
|
324
src/components/views/elements/Dropdown.js
Normal file
324
src/components/views/elements/Dropdown.js
Normal file
@ -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,
|
className: React.PropTypes.string,
|
||||||
labelClassName: React.PropTypes.string,
|
labelClassName: React.PropTypes.string,
|
||||||
placeholderClassName: React.PropTypes.string,
|
placeholderClassName: React.PropTypes.string,
|
||||||
|
// Overrides blurToSubmit if true
|
||||||
blurToCancel: React.PropTypes.bool,
|
blurToCancel: React.PropTypes.bool,
|
||||||
|
// Will cause onValueChanged(value, true) to fire on blur
|
||||||
|
blurToSubmit: React.PropTypes.bool,
|
||||||
editable: React.PropTypes.bool,
|
editable: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ module.exports = React.createClass({
|
|||||||
editable: true,
|
editable: true,
|
||||||
className: "mx_EditableText",
|
className: "mx_EditableText",
|
||||||
placeholderClassName: "mx_EditableText_placeholder",
|
placeholderClassName: "mx_EditableText_placeholder",
|
||||||
|
blurToSubmit: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -119,6 +123,7 @@ module.exports = React.createClass({
|
|||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
this.onValueChanged(false);
|
this.onValueChanged(false);
|
||||||
|
this.refs.editable_div.blur();
|
||||||
},
|
},
|
||||||
|
|
||||||
onValueChanged: function(shouldSubmit) {
|
onValueChanged: function(shouldSubmit) {
|
||||||
@ -182,13 +187,15 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onFinish: function(ev) {
|
onFinish: function(ev, shouldSubmit) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var submit = (ev.key === "Enter");
|
var submit = (ev.key === "Enter") || shouldSubmit;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
}, function() {
|
}, 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)
|
if (this.props.blurToCancel)
|
||||||
{this.cancelEdit();}
|
{this.cancelEdit();}
|
||||||
else
|
else
|
||||||
{this.onFinish(ev);}
|
{this.onFinish(ev, this.props.blurToSubmit);}
|
||||||
|
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,7 @@ export default class EditableTextContainer extends React.Component {
|
|||||||
<EditableText initialValue={this.state.value}
|
<EditableText initialValue={this.state.value}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
onValueChanged={this._onValueChanged}
|
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
|
/* callback to update the value. Called with a single argument: the new
|
||||||
* value. */
|
* value. */
|
||||||
onSubmit: React.PropTypes.func,
|
onSubmit: React.PropTypes.func,
|
||||||
|
|
||||||
|
/* should the input submit when focus is lost? */
|
||||||
|
blurToSubmit: React.PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
EditableTextContainer.defaultProps = {
|
EditableTextContainer.defaultProps = {
|
||||||
initialValue: "",
|
initialValue: "",
|
||||||
placeholder: "",
|
placeholder: "",
|
||||||
|
blurToSubmit: false,
|
||||||
onSubmit: function(v) {return q(); },
|
onSubmit: function(v) {return q(); },
|
||||||
};
|
};
|
||||||
|
@ -30,6 +30,8 @@ module.exports = React.createClass({
|
|||||||
avatarsMaxLength: React.PropTypes.number,
|
avatarsMaxLength: React.PropTypes.number,
|
||||||
// The minimum number of events needed to trigger summarisation
|
// The minimum number of events needed to trigger summarisation
|
||||||
threshold: React.PropTypes.number,
|
threshold: React.PropTypes.number,
|
||||||
|
// Called when the MELS expansion is toggled
|
||||||
|
onToggle: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
@ -63,6 +65,7 @@ module.exports = React.createClass({
|
|||||||
this.setState({
|
this.setState({
|
||||||
expanded: !this.state.expanded,
|
expanded: !this.state.expanded,
|
||||||
});
|
});
|
||||||
|
this.props.onToggle();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,7 +111,7 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||||
{summaries.join(", ")}
|
{summaries.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -264,7 +267,7 @@ module.exports = React.createClass({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="mx_MemberEventListSummary_avatars">
|
||||||
{avatars}
|
{avatars}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -397,31 +400,28 @@ module.exports = React.createClass({
|
|||||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||||
);
|
);
|
||||||
|
|
||||||
const avatars = this._renderAvatars(avatarMembers);
|
let summaryContainer = null;
|
||||||
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
if (!expanded) {
|
||||||
const toggleButton = (
|
summaryContainer = (
|
||||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
<div className="mx_EventTile_line">
|
||||||
{expanded ? 'collapse' : 'expand'}
|
<div className="mx_EventTile_info">
|
||||||
</a>
|
{this._renderAvatars(avatarMembers)}
|
||||||
);
|
{this._renderSummary(aggregate.names, orderedTransitionSequences)}
|
||||||
|
</div>
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const toggleButton = (
|
||||||
|
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||||
|
{expanded ? 'collapse' : 'expand'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberEventListSummary">
|
<div className="mx_MemberEventListSummary">
|
||||||
|
{toggleButton}
|
||||||
{summaryContainer}
|
{summaryContainer}
|
||||||
|
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||||
{expandedEvents}
|
{expandedEvents}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -16,17 +16,12 @@ limitations under the License.
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
|
import * as Roles from '../../../Roles';
|
||||||
var roles = {
|
|
||||||
0: 'User',
|
|
||||||
50: 'Moderator',
|
|
||||||
100: 'Admin',
|
|
||||||
};
|
|
||||||
|
|
||||||
var reverseRoles = {};
|
var reverseRoles = {};
|
||||||
Object.keys(roles).forEach(function(key) {
|
Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) {
|
||||||
reverseRoles[roles[key]] = key;
|
reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key;
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
@ -49,7 +44,7 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
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";
|
selectValue = "Custom";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
selectValue = roles[this.props.value] || "Custom";
|
selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||||
}
|
}
|
||||||
var select;
|
var select;
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
select = <span>{ selectValue }</span>;
|
select = <span>{ selectValue }</span>;
|
||||||
}
|
}
|
||||||
else {
|
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 =
|
||||||
<select ref="select"
|
<select ref="select"
|
||||||
value={ this.props.controlled ? selectValue : undefined }
|
value={ this.props.controlled ? selectValue : undefined }
|
||||||
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
||||||
onChange={ this.onSelectChange }>
|
onChange={ this.onSelectChange }>
|
||||||
<option value="User">User (0)</option>
|
{ options }
|
||||||
<option value="Moderator">Moderator (50)</option>
|
|
||||||
<option value="Admin">Admin (100)</option>
|
|
||||||
<option value="Custom">Custom level</option>
|
|
||||||
</select>;
|
</select>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
125
src/components/views/login/CountryDropdown.js
Normal file
125
src/components/views/login/CountryDropdown.js
Normal file
@ -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 React from 'react';
|
||||||
|
import url from 'url';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
|
|
||||||
/* This file contains a collection of components which are used by the
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* 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
|
* Call getEntryComponentForLoginType() to get a component suitable for a
|
||||||
* particular login type. Each component requires the same properties:
|
* 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
|
* loginType: the login type of the auth stage being attempted
|
||||||
* authSessionId: session id from the server
|
* 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
|
* stageParams: params from the server for the stage being attempted
|
||||||
* errorText: error message from a previous attempt to authenticate
|
* errorText: error message from a previous attempt to authenticate
|
||||||
* submitAuthDict: a function which will be called with the new auth dict
|
* submitAuthDict: a function which will be called with the new auth dict
|
||||||
* busy: a boolean indicating whether the auth logic is doing something
|
* busy: a boolean indicating whether the auth logic is doing something
|
||||||
* the user needs to wait for.
|
* 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):
|
* Each component may also provide the following functions (beyond the standard React ones):
|
||||||
* focus: set the input focus appropriately in the form.
|
* focus: set the input focus appropriately in the form.
|
||||||
@ -48,6 +68,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.object.isRequired,
|
||||||
submitAuthDict: React.PropTypes.func.isRequired,
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
errorText: React.PropTypes.string,
|
errorText: React.PropTypes.string,
|
||||||
// is the auth logic currently waiting for something to
|
// is the auth logic currently waiting for something to
|
||||||
@ -73,7 +94,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||||||
|
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||||
user: MatrixClientPeg.get().credentials.userId,
|
user: this.props.matrixClient.credentials.userId,
|
||||||
password: this.refs.passwordField.value,
|
password: this.refs.passwordField.value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -139,6 +160,7 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||||||
submitAuthDict: React.PropTypes.func.isRequired,
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
stageParams: React.PropTypes.object.isRequired,
|
stageParams: React.PropTypes.object.isRequired,
|
||||||
errorText: React.PropTypes.string,
|
errorText: React.PropTypes.string,
|
||||||
|
busy: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCaptchaResponse: function(response) {
|
_onCaptchaResponse: function(response) {
|
||||||
@ -149,6 +171,11 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (this.props.busy) {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||||
var sitePublicKey = this.props.stageParams.public_key;
|
var sitePublicKey = this.props.stageParams.public_key;
|
||||||
return (
|
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({
|
export const FallbackAuthEntry = React.createClass({
|
||||||
displayName: 'FallbackAuthEntry',
|
displayName: 'FallbackAuthEntry',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.object.isRequired,
|
||||||
authSessionId: React.PropTypes.string.isRequired,
|
authSessionId: React.PropTypes.string.isRequired,
|
||||||
loginType: React.PropTypes.string.isRequired,
|
loginType: React.PropTypes.string.isRequired,
|
||||||
submitAuthDict: React.PropTypes.func.isRequired,
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
@ -189,7 +420,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_onShowFallbackClick: function() {
|
_onShowFallbackClick: function() {
|
||||||
var url = MatrixClientPeg.get().getFallbackAuthUrl(
|
var url = this.props.matrixClient.getFallbackAuthUrl(
|
||||||
this.props.loginType,
|
this.props.loginType,
|
||||||
this.props.authSessionId
|
this.props.authSessionId
|
||||||
);
|
);
|
||||||
@ -199,7 +430,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||||||
_onReceiveMessage: function(event) {
|
_onReceiveMessage: function(event) {
|
||||||
if (
|
if (
|
||||||
event.data === "authDone" &&
|
event.data === "authDone" &&
|
||||||
event.origin === MatrixClientPeg.get().getHomeserverUrl()
|
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||||
) {
|
) {
|
||||||
this.props.submitAuthDict({});
|
this.props.submitAuthDict({});
|
||||||
}
|
}
|
||||||
@ -220,6 +451,8 @@ export const FallbackAuthEntry = React.createClass({
|
|||||||
const AuthEntryComponents = [
|
const AuthEntryComponents = [
|
||||||
PasswordAuthEntry,
|
PasswordAuthEntry,
|
||||||
RecaptchaAuthEntry,
|
RecaptchaAuthEntry,
|
||||||
|
EmailIdentityAuthEntry,
|
||||||
|
MsisdnAuthEntry,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getEntryComponentForLoginType(loginType) {
|
export function getEntryComponentForLoginType(loginType) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -17,6 +18,7 @@ limitations under the License.
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import sdk from '../../../index';
|
||||||
import {field_input_incorrect} from '../../../UiEffects';
|
import {field_input_incorrect} from '../../../UiEffects';
|
||||||
|
|
||||||
|
|
||||||
@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||||
initialUsername: React.PropTypes.string,
|
initialUsername: React.PropTypes.string,
|
||||||
|
initialPhoneCountry: React.PropTypes.string,
|
||||||
|
initialPhoneNumber: React.PropTypes.string,
|
||||||
initialPassword: React.PropTypes.string,
|
initialPassword: React.PropTypes.string,
|
||||||
onUsernameChanged: React.PropTypes.func,
|
onUsernameChanged: React.PropTypes.func,
|
||||||
|
onPhoneCountryChanged: React.PropTypes.func,
|
||||||
|
onPhoneNumberChanged: React.PropTypes.func,
|
||||||
onPasswordChanged: React.PropTypes.func,
|
onPasswordChanged: React.PropTypes.func,
|
||||||
loginIncorrect: React.PropTypes.bool,
|
loginIncorrect: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
return {
|
return {
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onPasswordChanged: function() {},
|
onPasswordChanged: function() {},
|
||||||
|
onPhoneCountryChanged: function() {},
|
||||||
|
onPhoneNumberChanged: function() {},
|
||||||
initialUsername: "",
|
initialUsername: "",
|
||||||
|
initialPhoneCountry: "",
|
||||||
|
initialPhoneNumber: "",
|
||||||
initialPassword: "",
|
initialPassword: "",
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
};
|
};
|
||||||
@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
return {
|
return {
|
||||||
username: this.props.initialUsername,
|
username: this.props.initialUsername,
|
||||||
password: this.props.initialPassword,
|
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) {
|
onSubmitForm: function(ev) {
|
||||||
ev.preventDefault();
|
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) {
|
onUsernameChanged: function(ev) {
|
||||||
@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
this.props.onUsernameChanged(ev.target.value);
|
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) {
|
onPasswordChanged: function(ev) {
|
||||||
this.setState({password: ev.target.value});
|
this.setState({password: ev.target.value});
|
||||||
this.props.onPasswordChanged(ev.target.value);
|
this.props.onPasswordChanged(ev.target.value);
|
||||||
@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||||||
error: this.props.loginIncorrect,
|
error: this.props.loginIncorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<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
|
name="username" // make it a little easier for browser's remember-password
|
||||||
value={this.state.username} onChange={this.onUsernameChanged}
|
value={this.state.username} onChange={this.onUsernameChanged}
|
||||||
placeholder="Email or user name" autoFocus />
|
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 />
|
<br />
|
||||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,18 +15,19 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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');
|
const FIELD_EMAIL = 'field_email';
|
||||||
var UiEffects = require('../../../UiEffects');
|
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||||
var sdk = require('../../../index');
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
var Email = require('../../../email');
|
const FIELD_USERNAME = 'field_username';
|
||||||
var Modal = require("../../../Modal");
|
const FIELD_PASSWORD = 'field_password';
|
||||||
|
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||||
var FIELD_EMAIL = 'field_email';
|
|
||||||
var FIELD_USERNAME = 'field_username';
|
|
||||||
var FIELD_PASSWORD = 'field_password';
|
|
||||||
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pure UI component which displays a registration form.
|
* A pure UI component which displays a registration form.
|
||||||
@ -36,6 +38,8 @@ module.exports = React.createClass({
|
|||||||
propTypes: {
|
propTypes: {
|
||||||
// Values pre-filled in the input boxes when the component loads
|
// Values pre-filled in the input boxes when the component loads
|
||||||
defaultEmail: React.PropTypes.string,
|
defaultEmail: React.PropTypes.string,
|
||||||
|
defaultPhoneCountry: React.PropTypes.string,
|
||||||
|
defaultPhoneNumber: React.PropTypes.string,
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
defaultPassword: React.PropTypes.string,
|
defaultPassword: React.PropTypes.string,
|
||||||
teamsConfig: React.PropTypes.shape({
|
teamsConfig: React.PropTypes.shape({
|
||||||
@ -54,15 +58,13 @@ module.exports = React.createClass({
|
|||||||
// a different username will cause a fresh account to be generated.
|
// a different username will cause a fresh account to be generated.
|
||||||
guestUsername: React.PropTypes.string,
|
guestUsername: React.PropTypes.string,
|
||||||
|
|
||||||
showEmail: React.PropTypes.bool,
|
|
||||||
minPasswordLength: React.PropTypes.number,
|
minPasswordLength: React.PropTypes.number,
|
||||||
onError: React.PropTypes.func,
|
onError: React.PropTypes.func,
|
||||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
showEmail: false,
|
|
||||||
minPasswordLength: 6,
|
minPasswordLength: 6,
|
||||||
onError: function(e) {
|
onError: function(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -74,6 +76,8 @@ module.exports = React.createClass({
|
|||||||
return {
|
return {
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
selectedTeam: null,
|
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_CONFIRM);
|
||||||
this.validateField(FIELD_PASSWORD);
|
this.validateField(FIELD_PASSWORD);
|
||||||
this.validateField(FIELD_USERNAME);
|
this.validateField(FIELD_USERNAME);
|
||||||
|
this.validateField(FIELD_PHONE_NUMBER);
|
||||||
this.validateField(FIELD_EMAIL);
|
this.validateField(FIELD_EMAIL);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -121,6 +126,8 @@ module.exports = React.createClass({
|
|||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: email,
|
email: email,
|
||||||
|
phoneCountry: this.state.phoneCountry,
|
||||||
|
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@ -174,8 +181,13 @@ module.exports = React.createClass({
|
|||||||
showSupportEmail: false,
|
showSupportEmail: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const valid = email === '' || Email.looksValid(email);
|
const emailValid = email === '' || Email.looksValid(email);
|
||||||
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
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;
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
@ -227,7 +239,7 @@ module.exports = React.createClass({
|
|||||||
fieldValid[field_id] = val;
|
fieldValid[field_id] = val;
|
||||||
this.setState({fieldValid: fieldValid});
|
this.setState({fieldValid: fieldValid});
|
||||||
if (!val) {
|
if (!val) {
|
||||||
UiEffects.field_input_incorrect(this.fieldElementById(field_id));
|
field_input_incorrect(this.fieldElementById(field_id));
|
||||||
this.props.onError(error_code);
|
this.props.onError(error_code);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -236,6 +248,8 @@ module.exports = React.createClass({
|
|||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
return this.refs.email;
|
return this.refs.email;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
return this.refs.phoneNumber;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
return this.refs.username;
|
return this.refs.username;
|
||||||
case FIELD_PASSWORD:
|
case FIELD_PASSWORD:
|
||||||
@ -245,8 +259,8 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_classForField: function(field_id, baseClass) {
|
_classForField: function(field_id, ...baseClasses) {
|
||||||
let cls = baseClass || '';
|
let cls = baseClasses.join(' ');
|
||||||
if (this.state.fieldValid[field_id] === false) {
|
if (this.state.fieldValid[field_id] === false) {
|
||||||
if (cls) cls += ' ';
|
if (cls) cls += ' ';
|
||||||
cls += 'error';
|
cls += 'error';
|
||||||
@ -254,46 +268,71 @@ module.exports = React.createClass({
|
|||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onPhoneCountryChange(newVal) {
|
||||||
|
this.setState({
|
||||||
|
phoneCountry: newVal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var emailSection, belowEmailSection, registerButton;
|
|
||||||
if (this.props.showEmail) {
|
const emailSection = (
|
||||||
emailSection = (
|
<div>
|
||||||
<input type="text" ref="email"
|
<input type="text" ref="email"
|
||||||
autoFocus={true} placeholder="Email address (optional)"
|
autoFocus={true} placeholder="Email address (optional)"
|
||||||
defaultValue={this.props.defaultEmail}
|
defaultValue={this.props.defaultEmail}
|
||||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
value={self.state.email}/>
|
value={self.state.email}/>
|
||||||
);
|
</div>
|
||||||
if (this.props.teamsConfig) {
|
);
|
||||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
let belowEmailSection;
|
||||||
belowEmailSection = (
|
if (this.props.teamsConfig) {
|
||||||
<p className="mx_Login_support">
|
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||||
Sorry, but your university is not registered with us just yet.
|
belowEmailSection = (
|
||||||
Email us on
|
<p className="mx_Login_support">
|
||||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
Sorry, but your university is not registered with us just yet.
|
||||||
{this.props.teamsConfig.supportEmail}
|
Email us on
|
||||||
</a>
|
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
{this.props.teamsConfig.supportEmail}
|
||||||
</p>
|
</a>
|
||||||
);
|
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||||
} else if (this.state.selectedTeam) {
|
</p>
|
||||||
belowEmailSection = (
|
);
|
||||||
<p className="mx_Login_support">
|
} else if (this.state.selectedTeam) {
|
||||||
You are registering with {this.state.selectedTeam.name}
|
belowEmailSection = (
|
||||||
</p>
|
<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) {
|
if (this.props.guestUsername) {
|
||||||
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
||||||
}
|
}
|
||||||
@ -303,6 +342,7 @@ module.exports = React.createClass({
|
|||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{emailSection}
|
{emailSection}
|
||||||
{belowEmailSection}
|
{belowEmailSection}
|
||||||
|
{phoneSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
|
@ -25,6 +25,7 @@ import sdk from '../../../index';
|
|||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody',
|
||||||
@ -42,7 +43,7 @@ module.exports = React.createClass({
|
|||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: null
|
error: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ module.exports = React.createClass({
|
|||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
const ImageView = sdk.getComponent("elements.ImageView");
|
||||||
const params = {
|
const params = {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
mxEvent: this.props.mxEvent
|
mxEvent: this.props.mxEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (content.info) {
|
if (content.info) {
|
||||||
@ -70,22 +71,26 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
_isGif: function() {
|
_isGif: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
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) {
|
onImageEnter: function(e) {
|
||||||
if (!this._isGif()) {
|
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var imgElement = e.target;
|
const imgElement = e.target;
|
||||||
imgElement.src = this._getContentUrl();
|
imgElement.src = this._getContentUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
onImageLeave: function(e) {
|
onImageLeave: function(e) {
|
||||||
if (!this._isGif()) {
|
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var imgElement = e.target;
|
const imgElement = e.target;
|
||||||
imgElement.src = this._getThumbUrl();
|
imgElement.src = this._getThumbUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -101,6 +106,7 @@ module.exports = React.createClass({
|
|||||||
_getThumbUrl: function() {
|
_getThumbUrl: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
|
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||||
if (this.state.decryptedThumbnailUrl) {
|
if (this.state.decryptedThumbnailUrl) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
}
|
}
|
||||||
@ -115,15 +121,15 @@ module.exports = React.createClass({
|
|||||||
this.fixupHeight();
|
this.fixupHeight();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
var thumbnailPromise = q(null);
|
let thumbnailPromise = q(null);
|
||||||
if (content.info.thumbnail_file) {
|
if (content.info.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file
|
content.info.thumbnail_file,
|
||||||
).then(function(blob) {
|
).then(function(blob) {
|
||||||
return readBlobAsDataUri(blob);
|
return readBlobAsDataUri(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var decryptedBlob;
|
let decryptedBlob;
|
||||||
thumbnailPromise.then((thumbnailUrl) => {
|
thumbnailPromise.then((thumbnailUrl) => {
|
||||||
return decryptFile(content.file).then(function(blob) {
|
return decryptFile(content.file).then(function(blob) {
|
||||||
decryptedBlob = 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
|
// 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);
|
//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) {
|
if (content.info) {
|
||||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
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) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
|
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
// The attachment is decrypted in componentDidMount.
|
// The attachment is decrypted in componentDidMount.
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a spinner.
|
||||||
@ -210,7 +215,12 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
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) {
|
if (thumbUrl) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,6 +23,7 @@ import Model from '../../../Modal';
|
|||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MVideoBody',
|
displayName: 'MVideoBody',
|
||||||
@ -152,11 +153,11 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
const thumbUrl = this._getThumbUrl();
|
const thumbUrl = this._getThumbUrl();
|
||||||
|
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
||||||
var height = null;
|
let height = null;
|
||||||
var width = null;
|
let width = null;
|
||||||
var poster = null;
|
let poster = null;
|
||||||
var preload = "metadata";
|
let preload = "metadata";
|
||||||
if (content.info) {
|
if (content.info) {
|
||||||
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
||||||
if (scale) {
|
if (scale) {
|
||||||
@ -169,11 +170,10 @@ module.exports = React.createClass({
|
|||||||
preload = "none";
|
preload = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVideoBody">
|
<span className="mx_MVideoBody">
|
||||||
<video className="mx_MVideoBody" src={contentUrl} alt={content.body}
|
<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}>
|
height={height} width={width} poster={poster}>
|
||||||
</video>
|
</video>
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||||
|
@ -16,17 +16,18 @@ limitations under the License.
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require('react-dom');
|
import ReactDOM from 'react-dom';
|
||||||
var highlight = require('highlight.js');
|
import highlight from 'highlight.js';
|
||||||
var HtmlUtils = require('../../../HtmlUtils');
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
var linkify = require('linkifyjs');
|
import * as linkify from 'linkifyjs';
|
||||||
var linkifyElement = require('linkifyjs/element');
|
import linkifyElement from 'linkifyjs/element';
|
||||||
var linkifyMatrix = require('../../../linkify-matrix');
|
import linkifyMatrix from '../../../linkify-matrix';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
var Modal = require("../../../Modal");
|
import Modal from '../../../Modal';
|
||||||
var SdkConfig = require('../../../SdkConfig');
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
@ -131,7 +132,8 @@ module.exports = React.createClass({
|
|||||||
links.push(node);
|
links.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (node.tagName === "PRE" || node.tagName === "CODE") {
|
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||||
|
node.tagName === "BLOCKQUOTE") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else if (node.children && node.children.length) {
|
else if (node.children && node.children.length) {
|
||||||
@ -187,6 +189,15 @@ module.exports = React.createClass({
|
|||||||
this.forceUpdate();
|
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() {
|
getEventTileOps: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return {
|
return {
|
||||||
@ -273,7 +284,15 @@ module.exports = React.createClass({
|
|||||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
return (
|
return (
|
||||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
<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 }
|
{ widgets }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -22,10 +22,10 @@ module.exports = React.createClass({
|
|||||||
displayName: 'UnknownBody',
|
displayName: 'UnknownBody',
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var content = this.props.mxEvent.getContent();
|
const text = this.props.mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<span className="mx_UnknownBody">
|
<span className="mx_UnknownBody" title="Redacted or unknown message type">
|
||||||
{content.body}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -25,18 +25,10 @@ var TextForEvent = require('../../../TextForEvent');
|
|||||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||||
|
|
||||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
var dispatcher = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
var ObjectUtils = require('../../../ObjectUtils');
|
var ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
|
||||||
var bounce = false;
|
|
||||||
try {
|
|
||||||
if (global.localStorage) {
|
|
||||||
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventTileTypes = {
|
var eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
'm.room.message': 'messages.MessageEvent',
|
||||||
'm.room.member' : 'messages.TextualEvent',
|
'm.room.member' : 'messages.TextualEvent',
|
||||||
@ -48,6 +40,7 @@ var eventTileTypes = {
|
|||||||
'm.room.third_party_invite' : 'messages.TextualEvent',
|
'm.room.third_party_invite' : 'messages.TextualEvent',
|
||||||
'm.room.history_visibility' : 'messages.TextualEvent',
|
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||||
'm.room.encryption' : 'messages.TextualEvent',
|
'm.room.encryption' : 'messages.TextualEvent',
|
||||||
|
'm.room.power_levels' : 'messages.TextualEvent',
|
||||||
};
|
};
|
||||||
|
|
||||||
var MAX_READ_AVATARS = 5;
|
var MAX_READ_AVATARS = 5;
|
||||||
@ -73,6 +66,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
mxEvent: React.PropTypes.object.isRequired,
|
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
|
/* true if this is a continuation of the previous event (which has the
|
||||||
* effect of not showing another avatar/displayname
|
* effect of not showing another avatar/displayname
|
||||||
*/
|
*/
|
||||||
@ -285,9 +284,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getReadAvatars: function() {
|
getReadAvatars: function() {
|
||||||
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||||
var avatars = [];
|
const avatars = [];
|
||||||
var left = 0;
|
const receiptOffset = 15;
|
||||||
|
let left = 0;
|
||||||
|
|
||||||
// It's possible that the receipt was sent several days AFTER the event.
|
// 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,
|
// 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) {
|
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
|
||||||
hidden = false;
|
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 userId = receipt.roomMember.userId;
|
||||||
var readReceiptInfo;
|
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)
|
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||||
avatars.unshift(
|
avatars.unshift(
|
||||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||||
@ -332,12 +337,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
showFullTimestamp={receipt.ts >= dayAfterEventTime}
|
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;
|
var remText;
|
||||||
if (!this.state.allReadAvatars) {
|
if (!this.state.allReadAvatars) {
|
||||||
@ -345,9 +344,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
if (remainder > 0) {
|
if (remainder > 0) {
|
||||||
remText = <span className="mx_EventTile_readAvatarRemainder"
|
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||||
onClick={this.toggleAllReadAvatars}
|
onClick={this.toggleAllReadAvatars}
|
||||||
style={{ left: left }}>{ remainder }+
|
style={{ right: -(left - receiptOffset) }}>{ remainder }+
|
||||||
</span>;
|
</span>;
|
||||||
left -= 15;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +357,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
|
|
||||||
onSenderProfileClick: function(event) {
|
onSenderProfileClick: function(event) {
|
||||||
var mxEvent = this.props.mxEvent;
|
var mxEvent = this.props.mxEvent;
|
||||||
dispatcher.dispatch({
|
dis.dispatch({
|
||||||
action: 'insert_displayname',
|
action: 'insert_displayname',
|
||||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
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() {
|
render: function() {
|
||||||
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
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 e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
|
||||||
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||||
|
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||||
|
|
||||||
var classes = classNames({
|
var classes = classNames({
|
||||||
mx_EventTile: true,
|
mx_EventTile: true,
|
||||||
@ -415,8 +425,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
mx_EventTile_verified: this.state.verified == true,
|
mx_EventTile_verified: this.state.verified == true,
|
||||||
mx_EventTile_unverified: this.state.verified == false,
|
mx_EventTile_unverified: this.state.verified == false,
|
||||||
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
|
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();
|
var readAvatars = this.getReadAvatars();
|
||||||
|
|
||||||
@ -489,6 +503,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
else if (e2eEnabled) {
|
else if (e2eEnabled) {
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
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") {
|
if (this.props.tileShape === "notif") {
|
||||||
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||||
@ -496,15 +512,15 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_EventTile_roomName">
|
<div className="mx_EventTile_roomName">
|
||||||
<a href={ permalink }>
|
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||||
{ room ? room.name : '' }
|
{ room ? room.name : '' }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
<a href={ permalink }>
|
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||||
{ sender }
|
{ sender }
|
||||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EventTile_line" >
|
<div className="mx_EventTile_line" >
|
||||||
@ -530,10 +546,14 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
onWidgetLoad={this.props.onWidgetLoad} />
|
onWidgetLoad={this.props.onWidgetLoad} />
|
||||||
</div>
|
</div>
|
||||||
<a className="mx_EventTile_senderDetailsLink" href={ permalink }>
|
<a
|
||||||
|
className="mx_EventTile_senderDetailsLink"
|
||||||
|
href={ permalink }
|
||||||
|
onClick={this.onPermalinkClicked}
|
||||||
|
>
|
||||||
<div className="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails">
|
||||||
{ sender }
|
{ sender }
|
||||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
{ timestamp }
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -548,8 +568,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
<div className="mx_EventTile_line">
|
<div className="mx_EventTile_line">
|
||||||
<a href={ permalink }>
|
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ e2e }
|
{ e2e }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
@ -567,7 +587,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
module.exports.haveTileForEvent = function(e) {
|
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()] == undefined) return false;
|
||||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||||
return TextForEvent.textForEvent(e) !== '';
|
return TextForEvent.textForEvent(e) !== '';
|
||||||
|
@ -218,11 +218,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onKick: function() {
|
onKick: function() {
|
||||||
|
const membership = this.props.member.membership;
|
||||||
|
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
Modal.createDialog(ConfirmUserActionDialog, {
|
Modal.createDialog(ConfirmUserActionDialog, {
|
||||||
member: this.props.member,
|
member: this.props.member,
|
||||||
action: 'Kick',
|
action: kickLabel,
|
||||||
askReason: true,
|
askReason: membership == "join",
|
||||||
danger: true,
|
danger: true,
|
||||||
onFinished: (proceed, reason) => {
|
onFinished: (proceed, reason) => {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
@ -237,9 +239,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
console.log("Kick success");
|
console.log("Kick success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Kick error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Kick error",
|
title: "Error",
|
||||||
description: err.message
|
description: "Failed to kick user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
).finally(()=>{
|
).finally(()=>{
|
||||||
@ -278,9 +281,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
console.log("Ban success");
|
console.log("Ban success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Ban error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Ban error",
|
title: "Error",
|
||||||
description: err.message,
|
description: "Failed to ban user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
).finally(()=>{
|
).finally(()=>{
|
||||||
@ -327,9 +331,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Mute toggle success");
|
console.log("Mute toggle success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
console.error("Mute error: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Mute error",
|
title: "Error",
|
||||||
description: err.message
|
description: "Failed to mute user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
).finally(()=>{
|
).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."
|
description: "This action cannot be performed by a guest user. Please register to be able to do this."
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
console.error("Toggle moderator error:" + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Moderator toggle error",
|
title: "Error",
|
||||||
description: err.message
|
description: "Failed to toggle moderator status",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -395,9 +401,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
console.log("Power change success");
|
console.log("Power change success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to change power level " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failure to change power level",
|
title: "Error",
|
||||||
description: err.message
|
description: "Failed to change power level",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
).finally(()=>{
|
).finally(()=>{
|
||||||
@ -553,6 +560,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomTileClick(roomId) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_renderDevices: function() {
|
_renderDevices: function() {
|
||||||
if (!this._enableDevices) {
|
if (!this._enableDevices) {
|
||||||
return null;
|
return null;
|
||||||
@ -569,7 +583,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
} else if (devices === null) {
|
} else if (devices === null) {
|
||||||
devComponents = "Unable to load device list";
|
devComponents = "Unable to load device list";
|
||||||
} else if (devices.length === 0) {
|
} else if (devices.length === 0) {
|
||||||
devComponents = "No registered devices";
|
devComponents = "No devices with registered encryption keys";
|
||||||
} else {
|
} else {
|
||||||
devComponents = [];
|
devComponents = [];
|
||||||
for (var i = 0; i < devices.length; i++) {
|
for (var i = 0; i < devices.length; i++) {
|
||||||
@ -613,6 +627,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
isInvite={me.membership == "invite"}
|
isInvite={me.membership == "invite"}
|
||||||
|
onClick={this.onRoomTileClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -91,8 +91,9 @@ export default class MessageComposer extends React.Component {
|
|||||||
this.refs.uploadInput.click();
|
this.refs.uploadInput.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUploadFileSelected(ev) {
|
onUploadFileSelected(files, isPasted) {
|
||||||
let files = ev.target.files;
|
if (!isPasted)
|
||||||
|
files = files.target.files;
|
||||||
|
|
||||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
@ -100,7 +101,7 @@ export default class MessageComposer extends React.Component {
|
|||||||
let fileList = [];
|
let fileList = [];
|
||||||
for (let i=0; i<files.length; i++) {
|
for (let i=0; i<files.length; i++) {
|
||||||
fileList.push(<li key={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>);
|
</li>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +172,7 @@ export default class MessageComposer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpArrow() {
|
onUpArrow() {
|
||||||
return this.refs.autocomplete.onUpArrow();
|
return this.refs.autocomplete.onUpArrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDownArrow() {
|
onDownArrow() {
|
||||||
@ -223,8 +224,8 @@ export default class MessageComposer extends React.Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let e2eImg, e2eTitle, e2eClass;
|
let e2eImg, e2eTitle, e2eClass;
|
||||||
|
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||||
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
|
if (roomIsEncrypted) {
|
||||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||||
e2eImg = 'img/e2e-verified.svg';
|
e2eImg = 'img/e2e-verified.svg';
|
||||||
e2eTitle = 'Encrypted room';
|
e2eTitle = 'Encrypted room';
|
||||||
@ -286,15 +287,20 @@ export default class MessageComposer extends React.Component {
|
|||||||
key="controls_formatting" />
|
key="controls_formatting" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const placeholderText = roomIsEncrypted ?
|
||||||
|
"Send an encrypted message…" : "Send a message (unencrypted)…";
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
ref={c => this.messageComposerInput = c}
|
ref={c => this.messageComposerInput = c}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
placeholder={placeholderText}
|
||||||
tryComplete={this._tryComplete}
|
tryComplete={this._tryComplete}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
|
onUploadFileSelected={this.onUploadFileSelected}
|
||||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||||
onContentChanged={this.onInputContentChanged}
|
onContentChanged={this.onInputContentChanged}
|
||||||
onInputStateChanged={this.onInputStateChanged} />,
|
onInputStateChanged={this.onInputStateChanged} />,
|
||||||
|
@ -84,6 +84,7 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
this.onAction = this.onAction.bind(this);
|
this.onAction = this.onAction.bind(this);
|
||||||
this.handleReturn = this.handleReturn.bind(this);
|
this.handleReturn = this.handleReturn.bind(this);
|
||||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||||
|
this.handlePastedFiles = this.handlePastedFiles.bind(this);
|
||||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||||
this.setEditorState = this.setEditorState.bind(this);
|
this.setEditorState = this.setEditorState.bind(this);
|
||||||
this.onUpArrow = this.onUpArrow.bind(this);
|
this.onUpArrow = this.onUpArrow.bind(this);
|
||||||
@ -475,6 +476,10 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePastedFiles(files) {
|
||||||
|
this.props.onUploadFileSelected(files, true);
|
||||||
|
}
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
@ -504,7 +509,7 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: 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;
|
let sendTextFn = this.client.sendTextMessage;
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
contentText = contentText.replace('/me', '');
|
contentText = contentText.replace('/me ', '');
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// 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;
|
sendHtmlFn = this.client.sendHtmlEmote;
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
@ -721,13 +726,14 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||||
<Editor ref="editor"
|
<Editor ref="editor"
|
||||||
placeholder="Type a message…"
|
placeholder={this.props.placeholder}
|
||||||
editorState={this.state.editorState}
|
editorState={this.state.editorState}
|
||||||
onChange={this.onEditorContentChanged}
|
onChange={this.onEditorContentChanged}
|
||||||
blockStyleFn={MessageComposerInput.getBlockStyle}
|
blockStyleFn={MessageComposerInput.getBlockStyle}
|
||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
|
handlePastedFiles={this.handlePastedFiles}
|
||||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||||
onTab={this.onTab}
|
onTab={this.onTab}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
@ -757,6 +763,8 @@ MessageComposerInput.propTypes = {
|
|||||||
|
|
||||||
onDownArrow: React.PropTypes.func,
|
onDownArrow: React.PropTypes.func,
|
||||||
|
|
||||||
|
onUploadFileSelected: React.PropTypes.func,
|
||||||
|
|
||||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
tryComplete: React.PropTypes.func,
|
tryComplete: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -34,16 +34,11 @@ export function onSendMessageFailed(err, room) {
|
|||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||||
if (err.name === "UnknownDeviceError") {
|
if (err.name === "UnknownDeviceError") {
|
||||||
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
dis.dispatch({
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
action: 'unknown_device_error',
|
||||||
devices: err.devices,
|
err: err,
|
||||||
room: room,
|
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({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
@ -70,6 +65,9 @@ export default React.createClass({
|
|||||||
|
|
||||||
// js-sdk Room object
|
// js-sdk Room object
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// The text to use a placeholder in the input box
|
||||||
|
placeholder: React.PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
@ -313,7 +311,7 @@ export default React.createClass({
|
|||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: err.message
|
description: "Server unavailable, overloaded, or something else went wrong.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -442,7 +440,7 @@ export default React.createClass({
|
|||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -115,9 +115,10 @@ module.exports = React.createClass({
|
|||||||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to set avatar: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to set avatar. " + errMsg
|
description: "Failed to set avatar.",
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -96,6 +96,12 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
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'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
||||||
label="People"
|
label="People"
|
||||||
editable={ false }
|
tagName="im.vector.fake.direct"
|
||||||
|
verb="tag direct chat"
|
||||||
|
editable={ true }
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
alwaysShowHeader={ true }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
@ -54,9 +54,10 @@ const BannedUser = React.createClass({
|
|||||||
this.props.member.roomId, this.props.member.userId,
|
this.props.member.roomId, this.props.member.userId,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to unban: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to unban",
|
title: "Error",
|
||||||
description: err.message,
|
description: "Failed to unban",
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var classNames = require('classnames');
|
var classNames = require('classnames');
|
||||||
var dis = require("../../../dispatcher");
|
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
@ -35,6 +34,7 @@ module.exports = React.createClass({
|
|||||||
propTypes: {
|
propTypes: {
|
||||||
connectDragSource: React.PropTypes.func,
|
connectDragSource: React.PropTypes.func,
|
||||||
connectDropTarget: React.PropTypes.func,
|
connectDropTarget: React.PropTypes.func,
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
isDragging: React.PropTypes.bool,
|
isDragging: React.PropTypes.bool,
|
||||||
|
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
@ -56,8 +56,7 @@ module.exports = React.createClass({
|
|||||||
return({
|
return({
|
||||||
hover : false,
|
hover : false,
|
||||||
badgeHover : false,
|
badgeHover : false,
|
||||||
notificationTagMenu: false,
|
menuDisplayed: false,
|
||||||
roomTagMenu: false,
|
|
||||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -100,10 +99,9 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
dis.dispatch({
|
if (this.props.onClick) {
|
||||||
action: 'view_room',
|
this.props.onClick(this.props.room.roomId);
|
||||||
room_id: this.props.room.roomId,
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseEnter: function() {
|
onMouseEnter: function() {
|
||||||
@ -137,62 +135,32 @@ module.exports = React.createClass({
|
|||||||
this.setState({ hover: false });
|
this.setState({ hover: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu');
|
var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||||
var elementRect = e.target.getBoundingClientRect();
|
var elementRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
var x = elementRect.right + window.pageXOffset + 3;
|
const x = elementRect.right + window.pageXOffset + 3;
|
||||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
|
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;
|
var self = this;
|
||||||
ContextualMenu.createMenu(NotificationStateMenu, {
|
ContextualMenu.createMenu(RoomTileContextMenu, {
|
||||||
menuWidth: 188,
|
chevronOffset: chevronOffset,
|
||||||
menuHeight: 126,
|
|
||||||
chevronOffset: 45,
|
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
room: this.props.room,
|
room: this.props.room,
|
||||||
onFinished: function() {
|
onFinished: function() {
|
||||||
self.setState({ notificationTagMenu: false });
|
self.setState({ menuDisplayed: false });
|
||||||
self.props.refreshSubList();
|
self.props.refreshSubList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.setState({ notificationTagMenu: true });
|
this.setState({ menuDisplayed: true });
|
||||||
}
|
}
|
||||||
// Prevent the RoomTile onClick event firing as well
|
// Prevent the RoomTile onClick event firing as well
|
||||||
e.stopPropagation();
|
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() {
|
render: function() {
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
var me = this.props.room.currentState.members[myUserId];
|
var me = this.props.room.currentState.members[myUserId];
|
||||||
@ -211,7 +179,7 @@ module.exports = React.createClass({
|
|||||||
'mx_RoomTile_unreadNotify': notifBadges,
|
'mx_RoomTile_unreadNotify': notifBadges,
|
||||||
'mx_RoomTile_highlight': mentionBadges,
|
'mx_RoomTile_highlight': mentionBadges,
|
||||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||||
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu,
|
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||||
'mx_RoomTile_noBadges': !badges,
|
'mx_RoomTile_noBadges': !badges,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,14 +187,9 @@ module.exports = React.createClass({
|
|||||||
'mx_RoomTile_avatar': true,
|
'mx_RoomTile_avatar': true,
|
||||||
});
|
});
|
||||||
|
|
||||||
var avatarContainerClasses = classNames({
|
|
||||||
'mx_RoomTile_avatar_container': true,
|
|
||||||
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
|
||||||
});
|
|
||||||
|
|
||||||
var badgeClasses = classNames({
|
var badgeClasses = classNames({
|
||||||
'mx_RoomTile_badge': true,
|
'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
|
// XXX: We should never display raw room IDs, but sometimes the
|
||||||
@ -237,7 +200,7 @@ module.exports = React.createClass({
|
|||||||
var badge;
|
var badge;
|
||||||
var badgeContent;
|
var badgeContent;
|
||||||
|
|
||||||
if (this.state.badgeHover || this.state.notificationTagMenu) {
|
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||||
badgeContent = "\u00B7\u00B7\u00B7";
|
badgeContent = "\u00B7\u00B7\u00B7";
|
||||||
} else if (badges) {
|
} else if (badges) {
|
||||||
var limitedCount = FormattingUtils.formatCount(notificationCount);
|
var limitedCount = FormattingUtils.formatCount(notificationCount);
|
||||||
@ -255,7 +218,7 @@ module.exports = React.createClass({
|
|||||||
var nameClasses = classNames({
|
var nameClasses = classNames({
|
||||||
'mx_RoomTile_name': true,
|
'mx_RoomTile_name': true,
|
||||||
'mx_RoomTile_invite': this.props.isInvite,
|
'mx_RoomTile_invite': this.props.isInvite,
|
||||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu,
|
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.selected) {
|
if (this.props.selected) {
|
||||||
@ -294,11 +257,9 @@ module.exports = React.createClass({
|
|||||||
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
<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}>
|
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
<div className={avatarClasses}>
|
<div className={avatarClasses}>
|
||||||
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
<div className="mx_RoomTile_avatar_container">
|
||||||
<div className={avatarContainerClasses}>
|
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
{directMessageIndicator}
|
||||||
{directMessageIndicator}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomTile_nameContainer">
|
<div className="mx_RoomTile_nameContainer">
|
||||||
|
@ -32,10 +32,7 @@ module.exports = React.createClass({
|
|||||||
<div className="mx_TopUnreadMessagesBar">
|
<div className="mx_TopUnreadMessagesBar">
|
||||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||||
onClick={this.props.onScrollUpClick}>
|
onClick={this.props.onScrollUpClick}>
|
||||||
<img src="img/scrollup.svg" width="24" height="24"
|
Jump to first unread message. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
||||||
alt="Scroll to unread messages"
|
|
||||||
title="Scroll to unread messages"/>
|
|
||||||
Unread messages. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
|
||||||
</div>
|
</div>
|
||||||
<img className="mx_TopUnreadMessagesBar_close"
|
<img className="mx_TopUnreadMessagesBar_close"
|
||||||
src="img/cancel.svg" width="18" height="18"
|
src="img/cancel.svg" width="18" height="18"
|
||||||
|
172
src/components/views/settings/AddPhoneNumber.js
Normal file
172
src/components/views/settings/AddPhoneNumber.js
Normal file
@ -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
|
<EditableTextContainer
|
||||||
getInitialValue={this._getDisplayName}
|
getInitialValue={this._getDisplayName}
|
||||||
placeholder="No display name"
|
placeholder="No display name"
|
||||||
|
blurToSubmit={true}
|
||||||
onSubmit={this._changeDisplayName} />
|
onSubmit={this._changeDisplayName} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -73,11 +73,17 @@ module.exports = React.createClass({
|
|||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
Changing password will currently reset any end-to-end encryption keys on all devices,
|
Changing password will currently reset any end-to-end encryption keys on all devices,
|
||||||
making encrypted chat history unreadable.
|
making encrypted chat history unreadable, unless you first export your room keys
|
||||||
This will be <a href="https://github.com/vector-im/riot-web/issues/2671">improved shortly</a>,
|
and re-import them afterwards.
|
||||||
but for now be warned.
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||||
</div>,
|
</div>,
|
||||||
button: "Continue",
|
button: "Continue",
|
||||||
|
extraButtons: [
|
||||||
|
<button className="mx_Dialog_primary"
|
||||||
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
|
Export E2E room keys
|
||||||
|
</button>
|
||||||
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
var authDict = {
|
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() {
|
onClickChange: function() {
|
||||||
var old_password = this.refs.old_input.value;
|
var old_password = this.refs.old_input.value;
|
||||||
var new_password = this.refs.new_input.value;
|
var new_password = this.refs.new_input.value;
|
||||||
|
@ -19,6 +19,9 @@ import React from 'react';
|
|||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
import DateUtils from '../../../DateUtils';
|
||||||
|
|
||||||
|
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
export default class DevicesPanelEntry extends React.Component {
|
export default class DevicesPanelEntry extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
@ -30,7 +33,6 @@ export default class DevicesPanelEntry extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
|
||||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||||
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
|
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
|
||||||
@ -53,8 +55,12 @@ export default class DevicesPanelEntry extends React.Component {
|
|||||||
_onDeleteClick() {
|
_onDeleteClick() {
|
||||||
this.setState({deleting: true});
|
this.setState({deleting: true});
|
||||||
|
|
||||||
// try without interactive auth to start off
|
if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) {
|
||||||
this._makeDeleteRequest(null).catch((error) => {
|
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 (this._unmounted) { return; }
|
||||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||||
// doesn't look like an interactive-auth failure
|
// 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");
|
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||||
|
|
||||||
Modal.createDialog(InteractiveAuthDialog, {
|
Modal.createDialog(InteractiveAuthDialog, {
|
||||||
|
matrixClient: MatrixClientPeg.get(),
|
||||||
authData: error.data,
|
authData: error.data,
|
||||||
makeRequest: this._makeDeleteRequest,
|
makeRequest: this._makeDeleteRequest,
|
||||||
});
|
});
|
||||||
@ -83,6 +90,9 @@ export default class DevicesPanelEntry extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_makeDeleteRequest(auth) {
|
_makeDeleteRequest(auth) {
|
||||||
|
this.context.authCache.auth = auth;
|
||||||
|
this.context.authCache.lastUpdate = Date.now();
|
||||||
|
|
||||||
const device = this.props.device;
|
const device = this.props.device;
|
||||||
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
|
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
|
||||||
() => {
|
() => {
|
||||||
@ -110,8 +120,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||||||
|
|
||||||
let lastSeen = "";
|
let lastSeen = "";
|
||||||
if (device.last_seen_ts) {
|
if (device.last_seen_ts) {
|
||||||
// todo: format the timestamp as "5 minutes ago" or whatever.
|
const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts));
|
||||||
const lastSeenDate = new Date(device.last_seen_ts);
|
|
||||||
lastSeen = device.last_seen_ip + " @ " +
|
lastSeen = device.last_seen_ip + " @ " +
|
||||||
lastSeenDate.toLocaleString();
|
lastSeenDate.toLocaleString();
|
||||||
}
|
}
|
||||||
@ -160,6 +169,10 @@ DevicesPanelEntry.propTypes = {
|
|||||||
onDeleted: React.PropTypes.func,
|
onDeleted: React.PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DevicesPanelEntry.contextTypes = {
|
||||||
|
authCache: React.PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
DevicesPanelEntry.defaultProps = {
|
DevicesPanelEntry.defaultProps = {
|
||||||
onDeleted: function() {},
|
onDeleted: function() {},
|
||||||
};
|
};
|
||||||
|
@ -102,9 +102,10 @@ function createRoom(opts) {
|
|||||||
});
|
});
|
||||||
return roomId;
|
return roomId;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
console.error("Failed to create room " + roomId + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failure to create room",
|
title: "Failure to create room",
|
||||||
description: err.toString()
|
description: "Server may be unavailable, overloaded, or you hit a bug.",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
|
|||||||
// anyone else really should be using matrix.to.
|
// anyone else really should be using matrix.to.
|
||||||
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||||
+ escapeRegExp(window.location.host + window.location.pathname) + "|"
|
+ escapeRegExp(window.location.host + window.location.pathname) + "|"
|
||||||
+ "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/"
|
+ "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/"
|
||||||
+ ")(#.*)";
|
+ ")(#.*)";
|
||||||
|
|
||||||
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
||||||
|
1273
src/phonenumber.js
Normal file
1273
src/phonenumber.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,7 @@ describe('InteractiveAuthDialog', function () {
|
|||||||
|
|
||||||
const dlg = ReactDOM.render(
|
const dlg = ReactDOM.render(
|
||||||
<InteractiveAuthDialog
|
<InteractiveAuthDialog
|
||||||
|
matrixClient={client}
|
||||||
authData={{
|
authData={{
|
||||||
session: "sess",
|
session: "sess",
|
||||||
flows: [
|
flows: [
|
||||||
@ -67,48 +68,49 @@ describe('InteractiveAuthDialog', function () {
|
|||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
/>, parentDiv);
|
/>, parentDiv);
|
||||||
|
|
||||||
// at this point there should be a password box and a submit button
|
// wait for a password box and a submit button
|
||||||
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
|
test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => {
|
||||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||||
dlg, "input"
|
dlg, "input"
|
||||||
);
|
);
|
||||||
let passwordNode;
|
let passwordNode;
|
||||||
let submitNode;
|
let submitNode;
|
||||||
for (const node of inputNodes) {
|
for (const node of inputNodes) {
|
||||||
if (node.type == 'password') {
|
if (node.type == 'password') {
|
||||||
passwordNode = node;
|
passwordNode = node;
|
||||||
} else if (node.type == 'submit') {
|
} else if (node.type == 'submit') {
|
||||||
submitNode = node;
|
submitNode = node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
expect(passwordNode).toExist();
|
||||||
expect(passwordNode).toExist();
|
expect(submitNode).toExist();
|
||||||
expect(submitNode).toExist();
|
|
||||||
|
|
||||||
// submit should be disabled
|
// submit should be disabled
|
||||||
expect(submitNode.disabled).toBe(true);
|
expect(submitNode.disabled).toBe(true);
|
||||||
|
|
||||||
// put something in the password box, and hit enter; that should
|
// put something in the password box, and hit enter; that should
|
||||||
// trigger a request
|
// trigger a request
|
||||||
passwordNode.value = "s3kr3t";
|
passwordNode.value = "s3kr3t";
|
||||||
ReactTestUtils.Simulate.change(passwordNode);
|
ReactTestUtils.Simulate.change(passwordNode);
|
||||||
expect(submitNode.disabled).toBe(false);
|
expect(submitNode.disabled).toBe(false);
|
||||||
ReactTestUtils.Simulate.submit(formNode, {});
|
ReactTestUtils.Simulate.submit(formNode, {});
|
||||||
|
|
||||||
expect(doRequest.callCount).toEqual(1);
|
expect(doRequest.callCount).toEqual(1);
|
||||||
expect(doRequest.calledWithExactly({
|
expect(doRequest.calledWithExactly({
|
||||||
session: "sess",
|
session: "sess",
|
||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
password: "s3kr3t",
|
password: "s3kr3t",
|
||||||
user: "@user:id",
|
user: "@user:id",
|
||||||
})).toBe(true);
|
})).toBe(true);
|
||||||
|
|
||||||
// there should now be a spinner
|
// there should now be a spinner
|
||||||
ReactTestUtils.findRenderedComponentWithType(
|
ReactTestUtils.findRenderedComponentWithType(
|
||||||
dlg, sdk.getComponent('elements.Spinner'),
|
dlg, sdk.getComponent('elements.Spinner'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// let the request complete
|
// let the request complete
|
||||||
q.delay(1).then(() => {
|
return q.delay(1);
|
||||||
|
}).then(() => {
|
||||||
expect(onFinished.callCount).toEqual(1);
|
expect(onFinished.callCount).toEqual(1);
|
||||||
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
||||||
}).done(done, done);
|
}).done(done, done);
|
||||||
|
@ -1,11 +1,51 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var sinon = require('sinon');
|
import sinon from 'sinon';
|
||||||
var q = require('q');
|
import q from 'q';
|
||||||
|
import ReactTestUtils from 'react-addons-test-utils';
|
||||||
|
|
||||||
var peg = require('../src/MatrixClientPeg.js');
|
import peg from '../src/MatrixClientPeg.js';
|
||||||
var jssdk = require('matrix-js-sdk');
|
import jssdk from 'matrix-js-sdk';
|
||||||
var MatrixEvent = jssdk.MatrixEvent;
|
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
|
* Perform common actions before each test case, e.g. printing the test case
|
||||||
@ -92,6 +132,7 @@ export function createTestClient() {
|
|||||||
sendTextMessage: () => q({}),
|
sendTextMessage: () => q({}),
|
||||||
sendHtmlMessage: () => q({}),
|
sendHtmlMessage: () => q({}),
|
||||||
getSyncState: () => "SYNCING",
|
getSyncState: () => "SYNCING",
|
||||||
|
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user