Merge branch 'develop' into rte-fixes
Conflicts: src/UserSettingsStore.js src/autocomplete/EmojiProvider.js src/components/views/rooms/MessageComposerInput.js
This commit is contained in:
commit
fe121126f5
88 changed files with 5170 additions and 1126 deletions
267
CHANGELOG.md
267
CHANGELOG.md
|
@ -1,3 +1,270 @@
|
||||||
|
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2)
|
||||||
|
|
||||||
|
* Fix bug where links to Riot would fail to open.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1)
|
||||||
|
|
||||||
|
* Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -135,17 +135,24 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
noParse: [
|
noParse: [
|
||||||
|
// for cross platform compatibility use [\\\/] as the path separator
|
||||||
|
// this ensures that the regex trips on both Windows and *nix
|
||||||
|
|
||||||
// don't parse the languages within highlight.js. They
|
// don't parse the languages within highlight.js. They
|
||||||
// cause stack overflows
|
// cause stack overflows
|
||||||
// (https://github.com/webpack/webpack/issues/1721), and
|
// (https://github.com/webpack/webpack/issues/1721), and
|
||||||
// there is no need for webpack to parse them - they can
|
// there is no need for webpack to parse them - they can
|
||||||
// just be included as-is.
|
// just be included as-is.
|
||||||
/highlight\.js\/lib\/languages/,
|
/highlight\.js[\\\/]lib[\\\/]languages/,
|
||||||
|
|
||||||
|
// olm takes ages for webpack to process, and it's already heavily
|
||||||
|
// optimised, so there is little to gain by us uglifying it.
|
||||||
|
/olm[\\\/](javascript[\\\/])?olm\.js$/,
|
||||||
|
|
||||||
// also disable parsing for sinon, because it
|
// also disable parsing for sinon, because it
|
||||||
// tries to do voodoo with 'require' which upsets
|
// tries to do voodoo with 'require' which upsets
|
||||||
// webpack (https://github.com/webpack/webpack/issues/304)
|
// webpack (https://github.com/webpack/webpack/issues/304)
|
||||||
/sinon\/pkg\/sinon\.js$/,
|
/sinon[\\\/]pkg[\\\/]sinon\.js$/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.6",
|
"version": "0.8.8",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -32,8 +32,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "scripts/reskindex.js -h header",
|
"reskindex": "scripts/reskindex.js -h header",
|
||||||
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
|
"build": "babel src -d lib --source-maps",
|
||||||
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
|
"start": "babel src -w -d lib --source-maps",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.3",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "3.5.6",
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^8.9.1",
|
"highlight.js": "^8.9.1",
|
||||||
|
@ -63,11 +63,12 @@
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
|
"prop-types": "^15.5.8",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
var exec = require('child_process').exec;
|
|
||||||
|
|
||||||
// Makes sure the babel executable in the path is babel 6 (or greater), not
|
|
||||||
// babel 5, which it is if you upgrade from an older version of react-sdk and
|
|
||||||
// run 'npm install' since the package has changed to babel-cli, so 'babel'
|
|
||||||
// remains installed and the executable in node_modules/.bin remains as babel
|
|
||||||
// 5.
|
|
||||||
|
|
||||||
exec("babel -V", function (error, stdout, stderr) {
|
|
||||||
if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
|
|
||||||
console.log("\033[31m\033[1m"+
|
|
||||||
'*****************************************\n'+
|
|
||||||
'* matrix-react-sdk has moved to babel 6 *\n'+
|
|
||||||
'* Please "rm -rf node_modules && npm i" *\n'+
|
|
||||||
'* then restore links as appropriate *\n'+
|
|
||||||
'*****************************************\n'+
|
|
||||||
"\033[91m");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -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;
|
||||||
|
|
|
@ -22,8 +22,8 @@ module.exports = {
|
||||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||||
var url = member.getAvatarUrl(
|
var url = member.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
width,
|
Math.floor(width * window.devicePixelRatio),
|
||||||
height,
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod,
|
resizeMethod,
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
|
@ -40,7 +40,9 @@ module.exports = {
|
||||||
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||||
var url = ContentRepo.getHttpUriForMxc(
|
var url = ContentRepo.getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||||
width, height, resizeMethod
|
Math.floor(width * window.devicePixelRatio),
|
||||||
|
Math.floor(height * window.devicePixelRatio),
|
||||||
|
resizeMethod
|
||||||
);
|
);
|
||||||
if (!url || url.length === 0) {
|
if (!url || url.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -57,4 +59,3 @@ module.exports = {
|
||||||
return 'img/' + images[total % images.length] + '.png';
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,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. " + ((err && err.message) ? err.message : ""),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
62
src/ConstantTimeDispatcher.js
Normal file
62
src/ConstantTimeDispatcher.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// singleton which dispatches invocations of a given type & argument
|
||||||
|
// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
|
||||||
|
//
|
||||||
|
// This means you can have a single point which listens for an EventEmitter event
|
||||||
|
// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
|
||||||
|
// having each RoomTile register for the EventEmitter event and having to
|
||||||
|
// iterate over all of them.
|
||||||
|
class ConstantTimeDispatcher {
|
||||||
|
constructor() {
|
||||||
|
// type -> arg -> [ listener(arg, params) ]
|
||||||
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
register(type, arg, listener) {
|
||||||
|
if (!this.listeners[type]) this.listeners[type] = {};
|
||||||
|
if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
|
||||||
|
this.listeners[type][arg].push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(type, arg, listener) {
|
||||||
|
if (this.listeners[type] && this.listeners[type][arg]) {
|
||||||
|
var i = this.listeners[type][arg].indexOf(listener);
|
||||||
|
if (i > -1) {
|
||||||
|
this.listeners[type][arg].splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(type, arg, params) {
|
||||||
|
if (!this.listeners[type] || !this.listeners[type][arg]) {
|
||||||
|
//console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.listeners[type][arg].forEach(listener=>{
|
||||||
|
listener.call(arg, params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.constantTimeDispatcher) {
|
||||||
|
global.constantTimeDispatcher = new ConstantTimeDispatcher();
|
||||||
|
}
|
||||||
|
module.exports = global.constantTimeDispatcher;
|
|
@ -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,
|
||||||
|
|
|
@ -25,6 +25,9 @@ import emojione from 'emojione';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
emojione.imagePathSVG = 'emojione/svg/';
|
emojione.imagePathSVG = 'emojione/svg/';
|
||||||
|
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||||
|
emojione.imagePathPNG = 'emojione/png/';
|
||||||
|
// Use SVGs for emojis
|
||||||
emojione.imageType = 'svg';
|
emojione.imageType = 'svg';
|
||||||
|
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||||
|
@ -58,6 +61,29 @@ 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 useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
|
||||||
|
* @param unicode {integer} One or more integers representing unicode characters
|
||||||
|
* @returns A img node with the corresponding emoji
|
||||||
|
*/
|
||||||
|
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||||
|
const fileName = unicode.map((u) => {
|
||||||
|
return u.toString(16);
|
||||||
|
}).join('-');
|
||||||
|
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
|
||||||
|
const fileType = useSvg ? 'svg' : 'png';
|
||||||
|
return <img
|
||||||
|
alt={alt}
|
||||||
|
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stripParagraphs(html: string): string {
|
export function stripParagraphs(html: string): string {
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
@ -85,8 +111,7 @@ var sanitizeHtmlParams = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
'del', // for markdown
|
'del', // for markdown
|
||||||
// deliberately no h1/h2 to stop people shouting.
|
'h1', 'h2', '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', 'span',
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
|
||||||
],
|
],
|
||||||
|
@ -98,6 +123,7 @@ var sanitizeHtmlParams = {
|
||||||
// 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'],
|
||||||
|
|
|
@ -32,4 +32,5 @@ module.exports = {
|
||||||
DELETE: 46,
|
DELETE: 46,
|
||||||
KEY_D: 68,
|
KEY_D: 68,
|
||||||
KEY_E: 69,
|
KEY_E: 69,
|
||||||
|
KEY_K: 75,
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,7 +49,7 @@ import sdk from './index';
|
||||||
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
|
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
|
||||||
* turn will raise on_logged_in and will_start_client events.
|
* turn will raise on_logged_in and will_start_client events.
|
||||||
*
|
*
|
||||||
* It returns a promise which resolves when the above process completes.
|
* @param {object} opts
|
||||||
*
|
*
|
||||||
* @param {object} opts.realQueryParams: string->string map of the
|
* @param {object} opts.realQueryParams: string->string map of the
|
||||||
* query-parameters extracted from the real query-string of the starting
|
* query-parameters extracted from the real query-string of the starting
|
||||||
|
@ -67,6 +67,7 @@ import sdk from './index';
|
||||||
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
||||||
* true; defines the IS to use.
|
* true; defines the IS to use.
|
||||||
*
|
*
|
||||||
|
* @returns {Promise} a promise which resolves when the above process completes.
|
||||||
*/
|
*/
|
||||||
export function loadSession(opts) {
|
export function loadSession(opts) {
|
||||||
const realQueryParams = opts.realQueryParams || {};
|
const realQueryParams = opts.realQueryParams || {};
|
||||||
|
@ -127,7 +128,7 @@ export function loadSession(opts) {
|
||||||
|
|
||||||
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||||
// create a temporary MatrixClient to do the login
|
// create a temporary MatrixClient to do the login
|
||||||
var client = Matrix.createClient({
|
const client = Matrix.createClient({
|
||||||
baseUrl: queryParams.homeserver,
|
baseUrl: queryParams.homeserver,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
// Not really sure where the right home for it is.
|
// Not really sure where the right home for it is.
|
||||||
|
|
||||||
// create a temporary MatrixClient to do the login
|
// create a temporary MatrixClient to do the login
|
||||||
var client = Matrix.createClient({
|
const client = Matrix.createClient({
|
||||||
baseUrl: hsUrl,
|
baseUrl: hsUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
|
||||||
if (!localStorage) {
|
if (!localStorage) {
|
||||||
return q(false);
|
return q(false);
|
||||||
}
|
}
|
||||||
const hs_url = localStorage.getItem("mx_hs_url");
|
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||||
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||||
const access_token = localStorage.getItem("mx_access_token");
|
const accessToken = localStorage.getItem("mx_access_token");
|
||||||
const user_id = localStorage.getItem("mx_user_id");
|
const userId = localStorage.getItem("mx_user_id");
|
||||||
const device_id = localStorage.getItem("mx_device_id");
|
const deviceId = localStorage.getItem("mx_device_id");
|
||||||
|
|
||||||
let is_guest;
|
let isGuest;
|
||||||
if (localStorage.getItem("mx_is_guest") !== null) {
|
if (localStorage.getItem("mx_is_guest") !== null) {
|
||||||
is_guest = localStorage.getItem("mx_is_guest") === "true";
|
isGuest = localStorage.getItem("mx_is_guest") === "true";
|
||||||
} else {
|
} else {
|
||||||
// legacy key name
|
// legacy key name
|
||||||
is_guest = localStorage.getItem("matrix-is-guest") === "true";
|
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (access_token && user_id && hs_url) {
|
if (accessToken && userId && hsUrl) {
|
||||||
console.log("Restoring session for %s", user_id);
|
console.log("Restoring session for %s", userId);
|
||||||
try {
|
try {
|
||||||
setLoggedIn({
|
setLoggedIn({
|
||||||
userId: user_id,
|
userId: userId,
|
||||||
deviceId: device_id,
|
deviceId: deviceId,
|
||||||
accessToken: access_token,
|
accessToken: accessToken,
|
||||||
homeserverUrl: hs_url,
|
homeserverUrl: hsUrl,
|
||||||
identityServerUrl: is_url,
|
identityServerUrl: isUrl,
|
||||||
guest: is_guest,
|
guest: isGuest,
|
||||||
});
|
});
|
||||||
return q(true);
|
return q(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -273,9 +274,18 @@ export function initRtsClient(url) {
|
||||||
*/
|
*/
|
||||||
export function setLoggedIn(credentials) {
|
export function setLoggedIn(credentials) {
|
||||||
credentials.guest = Boolean(credentials.guest);
|
credentials.guest = Boolean(credentials.guest);
|
||||||
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
|
||||||
credentials.userId, credentials.guest,
|
console.log(
|
||||||
credentials.homeserverUrl);
|
"setLoggedIn: mxid:", credentials.userId,
|
||||||
|
"deviceId:", credentials.deviceId,
|
||||||
|
"guest:", credentials.guest,
|
||||||
|
"hs:", credentials.homeserverUrl,
|
||||||
|
);
|
||||||
|
// This is dispatched to indicate that the user is still in the process of logging in
|
||||||
|
// 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
|
// Resolves by default
|
||||||
let teamPromise = Promise.resolve(null);
|
let teamPromise = Promise.resolve(null);
|
||||||
|
@ -347,7 +357,7 @@ export function logout() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().logout().then(onLoggedOut,
|
MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||||
(err) => {
|
(err) => {
|
||||||
// Just throwing an error here is going to be very unhelpful
|
// Just throwing an error here is going to be very unhelpful
|
||||||
// if you're trying to log out because your server's down and
|
// if you're trying to log out because your server's down and
|
||||||
|
@ -358,8 +368,8 @@ export function logout() {
|
||||||
// change your password).
|
// change your password).
|
||||||
console.log("Failed to call logout API: token will not be invalidated");
|
console.log("Failed to call logout API: token will not be invalidated");
|
||||||
onLoggedOut();
|
onLoggedOut();
|
||||||
}
|
},
|
||||||
);
|
).done();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -415,7 +425,7 @@ export function stopMatrixClient() {
|
||||||
UserActivity.stop();
|
UserActivity.stop();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.stopClient();
|
cli.stopClient();
|
||||||
cli.removeAllListeners();
|
cli.removeAllListeners();
|
||||||
|
|
51
src/Login.js
51
src/Login.js
|
@ -105,21 +105,48 @@ export default class Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViaPassword(username, pass) {
|
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var isEmail = username.indexOf("@") > 0;
|
|
||||||
var loginParams = {
|
const isEmail = username.indexOf("@") > 0;
|
||||||
password: pass,
|
|
||||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
let identifier;
|
||||||
};
|
let legacyParams; // parameters added to support old HSes
|
||||||
if (isEmail) {
|
if (phoneCountry && phoneNumber) {
|
||||||
loginParams.medium = 'email';
|
identifier = {
|
||||||
loginParams.address = username;
|
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 {
|
} else {
|
||||||
loginParams.user = username;
|
identifier = {
|
||||||
|
type: 'm.id.user',
|
||||||
|
user: username,
|
||||||
|
};
|
||||||
|
legacyParams = {
|
||||||
|
user: username,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = this._createTemporaryClient();
|
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 client.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return q({
|
||||||
homeserverUrl: self._hsUrl,
|
homeserverUrl: self._hsUrl,
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -125,12 +137,12 @@ class MatrixClientPeg {
|
||||||
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
||||||
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
||||||
|
|
||||||
opts.store = new Matrix.IndexedDBStore(
|
opts.store = new Matrix.IndexedDBStore({
|
||||||
new Matrix.IndexedDBStoreBackend(window.indexedDB, "riot-web-sync"),
|
indexedDB: window.indexedDB,
|
||||||
new Matrix.SyncAccumulator(), {
|
dbName: "riot-web-sync",
|
||||||
localStorage: localStorage,
|
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,13 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import PlatformPeg from './PlatformPeg';
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import TextForEvent from './TextForEvent';
|
||||||
var PlatformPeg = require("./PlatformPeg");
|
import Avatar from './Avatar';
|
||||||
var TextForEvent = require('./TextForEvent');
|
import dis from './dispatcher';
|
||||||
var Avatar = require('./Avatar');
|
import sdk from './index';
|
||||||
var dis = require("./dispatcher");
|
import Modal from './Modal';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -30,7 +31,7 @@ var dis = require("./dispatcher");
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Notifier = {
|
const Notifier = {
|
||||||
notifsByRoom: {},
|
notifsByRoom: {},
|
||||||
|
|
||||||
notificationMessageForEvent: function(ev) {
|
notificationMessageForEvent: function(ev) {
|
||||||
|
@ -49,16 +50,16 @@ var Notifier = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg = this.notificationMessageForEvent(ev);
|
let msg = this.notificationMessageForEvent(ev);
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
var title;
|
let title;
|
||||||
if (!ev.sender || room.name == ev.sender.name) {
|
if (!ev.sender || room.name === ev.sender.name) {
|
||||||
title = room.name;
|
title = room.name;
|
||||||
// notificationMessageForEvent includes sender,
|
// notificationMessageForEvent includes sender,
|
||||||
// but we already have the sender here
|
// but we already have the sender here
|
||||||
if (ev.getContent().body) msg = ev.getContent().body;
|
if (ev.getContent().body) msg = ev.getContent().body;
|
||||||
} else if (ev.getType() == 'm.room.member') {
|
} else if (ev.getType() === 'm.room.member') {
|
||||||
// context is all in the message here, we don't need
|
// context is all in the message here, we don't need
|
||||||
// to display sender info
|
// to display sender info
|
||||||
title = room.name;
|
title = room.name;
|
||||||
|
@ -69,7 +70,7 @@ var Notifier = {
|
||||||
if (ev.getContent().body) msg = ev.getContent().body;
|
if (ev.getContent().body) msg = ev.getContent().body;
|
||||||
}
|
}
|
||||||
|
|
||||||
var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
|
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
|
||||||
ev.sender, 40, 40, 'crop'
|
ev.sender, 40, 40, 'crop'
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ var Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_playAudioNotification: function(ev, room) {
|
_playAudioNotification: function(ev, room) {
|
||||||
var e = document.getElementById("messageAudio");
|
const e = document.getElementById("messageAudio");
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
e.load();
|
||||||
e.play();
|
e.play();
|
||||||
|
@ -96,19 +97,19 @@ var Notifier = {
|
||||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
this.toolbarHidden = false;
|
this.toolbarHidden = false;
|
||||||
this.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() {
|
||||||
|
@ -122,7 +123,7 @@ var Notifier = {
|
||||||
// make sure that we persist the current setting audio_enabled setting
|
// make sure that we persist the current setting audio_enabled setting
|
||||||
// before changing anything
|
// before changing anything
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
if(global.localStorage.getItem('audio_notifications_enabled') == null) {
|
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
||||||
this.setAudioEnabled(this.isEnabled());
|
this.setAudioEnabled(this.isEnabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,6 +133,16 @@ var Notifier = {
|
||||||
plaf.requestNotificationPermission().done((result) => {
|
plaf.requestNotificationPermission().done((result) => {
|
||||||
if (result !== 'granted') {
|
if (result !== 'granted') {
|
||||||
// The permission request was dismissed or denied
|
// The permission request was dismissed or denied
|
||||||
|
const description = result === 'denied'
|
||||||
|
? 'Riot does not have permission to send you notifications'
|
||||||
|
+ ' - please check your browser settings'
|
||||||
|
: 'Riot was not given permission to send notifications'
|
||||||
|
+ ' - please try again';
|
||||||
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: 'Unable to enable Notifications',
|
||||||
|
description,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +153,7 @@ var Notifier = {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: true
|
value: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// clear the notifications_hidden flag, so that if notifications are
|
// clear the notifications_hidden flag, so that if notifications are
|
||||||
|
@ -153,7 +164,7 @@ var Notifier = {
|
||||||
global.localStorage.setItem('notifications_enabled', 'false');
|
global.localStorage.setItem('notifications_enabled', 'false');
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: false
|
value: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -166,7 +177,7 @@ var Notifier = {
|
||||||
|
|
||||||
if (!global.localStorage) return true;
|
if (!global.localStorage) return true;
|
||||||
|
|
||||||
var enabled = global.localStorage.getItem('notifications_enabled');
|
const enabled = global.localStorage.getItem('notifications_enabled');
|
||||||
if (enabled === null) return true;
|
if (enabled === null) return true;
|
||||||
return enabled === 'true';
|
return enabled === 'true';
|
||||||
},
|
},
|
||||||
|
@ -174,12 +185,12 @@ var Notifier = {
|
||||||
setAudioEnabled: function(enable) {
|
setAudioEnabled: function(enable) {
|
||||||
if (!global.localStorage) return;
|
if (!global.localStorage) return;
|
||||||
global.localStorage.setItem('audio_notifications_enabled',
|
global.localStorage.setItem('audio_notifications_enabled',
|
||||||
enable ? 'true' : 'false');
|
enable ? 'true' : 'false');
|
||||||
},
|
},
|
||||||
|
|
||||||
isAudioEnabled: function(enable) {
|
isAudioEnabled: function(enable) {
|
||||||
if (!global.localStorage) return true;
|
if (!global.localStorage) return true;
|
||||||
var enabled = global.localStorage.getItem(
|
const enabled = global.localStorage.getItem(
|
||||||
'audio_notifications_enabled');
|
'audio_notifications_enabled');
|
||||||
// default to true if the popups are enabled
|
// default to true if the popups are enabled
|
||||||
if (enabled === null) return this.isEnabled();
|
if (enabled === null) return this.isEnabled();
|
||||||
|
@ -193,7 +204,7 @@ var Notifier = {
|
||||||
// this is nothing to do with notifier_enabled
|
// this is nothing to do with notifier_enabled
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: this.isEnabled()
|
value: this.isEnabled(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the info to localStorage for persistent settings
|
// update the info to localStorage for persistent settings
|
||||||
|
@ -214,22 +225,21 @@ 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.isSyncing = false;
|
||||||
this.isPrepared = 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;
|
||||||
|
|
||||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||||
if (actions && actions.notify) {
|
if (actions && actions.notify) {
|
||||||
if (this.isEnabled()) {
|
if (this.isEnabled()) {
|
||||||
this._displayPopupNotification(ev, room);
|
this._displayPopupNotification(ev, room);
|
||||||
|
@ -241,7 +251,7 @@ var Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(ev, room) {
|
onRoomReceipt: function(ev, room) {
|
||||||
if (room.getUnreadNotificationCount() == 0) {
|
if (room.getUnreadNotificationCount() === 0) {
|
||||||
// ideally we would clear each notification when it was read,
|
// ideally we would clear each notification when it was read,
|
||||||
// but we have no way, given a read receipt, to know whether
|
// but we have no way, given a read receipt, to know whether
|
||||||
// the receipt comes before or after an event, so we can't
|
// the receipt comes before or after an event, so we can't
|
||||||
|
@ -256,7 +266,7 @@ var Notifier = {
|
||||||
}
|
}
|
||||||
delete this.notifsByRoom[room.roomId];
|
delete this.notifsByRoom[room.roomId];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!global.mxNotifier) {
|
if (!global.mxNotifier) {
|
||||||
|
|
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
|
||||||
|
|
|
@ -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();
|
||||||
|
@ -63,8 +65,8 @@ function textForMemberEvent(ev) {
|
||||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
||||||
return senderName + " set a profile picture";
|
return senderName + " set a profile picture";
|
||||||
} else {
|
} else {
|
||||||
// hacky hack for https://github.com/vector-im/vector-web/issues/2020
|
// suppress null rejoins
|
||||||
return senderName + " rejoined the room.";
|
return '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
|
@ -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 = {
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
|
/*
|
||||||
|
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 dis from './dispatcher';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
let isDialogOpen = false;
|
||||||
|
|
||||||
const onAction = function(payload) {
|
const onAction = function(payload) {
|
||||||
if (payload.action === 'unknown_device_error') {
|
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||||
|
isDialogOpen = true;
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createDialog(UnknownDeviceDialog, {
|
||||||
devices: payload.err.devices,
|
devices: payload.err.devices,
|
||||||
room: payload.room,
|
room: payload.room,
|
||||||
onFinished: (r) => {
|
onFinished: (r) => {
|
||||||
|
isDialogOpen = false;
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('UnknownDeviceDialog closed with '+r);
|
console.log('UnknownDeviceDialog closed with '+r);
|
||||||
|
|
|
@ -32,7 +32,7 @@ class UserActivity {
|
||||||
start() {
|
start() {
|
||||||
document.onmousedown = this._onUserActivity.bind(this);
|
document.onmousedown = this._onUserActivity.bind(this);
|
||||||
document.onmousemove = this._onUserActivity.bind(this);
|
document.onmousemove = this._onUserActivity.bind(this);
|
||||||
document.onkeypress = this._onUserActivity.bind(this);
|
document.onkeydown = this._onUserActivity.bind(this);
|
||||||
// can't use document.scroll here because that's only the document
|
// can't use document.scroll here because that's only the document
|
||||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||||
// also this needs to be the wheel event, not scroll, as scroll is
|
// also this needs to be the wheel event, not scroll, as scroll is
|
||||||
|
@ -50,7 +50,7 @@ class UserActivity {
|
||||||
stop() {
|
stop() {
|
||||||
document.onmousedown = undefined;
|
document.onmousedown = undefined;
|
||||||
document.onmousemove = undefined;
|
document.onmousemove = undefined;
|
||||||
document.onkeypress = undefined;
|
document.onkeydown = undefined;
|
||||||
window.removeEventListener('wheel', this._onUserActivity.bind(this),
|
window.removeEventListener('wheel', this._onUserActivity.bind(this),
|
||||||
{ passive: true, capture: true });
|
{ passive: true, capture: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var q = require("q");
|
import q from 'q';
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var Notifier = require("./Notifier");
|
import Notifier from './Notifier';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||||
|
@ -33,7 +33,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
return cli.getProfileInfo(cli.credentials.userId);
|
return cli.getProfileInfo(cli.credentials.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ module.exports = {
|
||||||
loadThreePids: function() {
|
loadThreePids: function() {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return q({
|
return q({
|
||||||
threepids: []
|
threepids: [],
|
||||||
}); // guests can't poke 3pid endpoint
|
}); // guests can't poke 3pid endpoint
|
||||||
}
|
}
|
||||||
return MatrixClientPeg.get().getThreePids();
|
return MatrixClientPeg.get().getThreePids();
|
||||||
|
@ -73,19 +73,19 @@ module.exports = {
|
||||||
Notifier.setAudioEnabled(enable);
|
Notifier.setAudioEnabled(enable);
|
||||||
},
|
},
|
||||||
|
|
||||||
changePassword: function(old_password, new_password) {
|
changePassword: function(oldPassword, newPassword) {
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var authDict = {
|
const authDict = {
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
user: cli.credentials.userId,
|
user: cli.credentials.userId,
|
||||||
password: old_password
|
password: oldPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
return cli.setPassword(authDict, new_password);
|
return cli.setPassword(authDict, newPassword);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Returns the email pusher (pusher of type 'email') for a given
|
* Returns the email pusher (pusher of type 'email') for a given
|
||||||
* email address. Email pushers all have the same app ID, so since
|
* email address. Email pushers all have the same app ID, so since
|
||||||
* pushers are unique over (app ID, pushkey), there will be at most
|
* pushers are unique over (app ID, pushkey), there will be at most
|
||||||
|
@ -95,8 +95,8 @@ module.exports = {
|
||||||
if (pushers === undefined) {
|
if (pushers === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
for (var i = 0; i < pushers.length; ++i) {
|
for (let i = 0; i < pushers.length; ++i) {
|
||||||
if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
|
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
||||||
return pushers[i];
|
return pushers[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ module.exports = {
|
||||||
addEmailPusher: function(address, data) {
|
addEmailPusher: function(address, data) {
|
||||||
return MatrixClientPeg.get().setPusher({
|
return MatrixClientPeg.get().setPusher({
|
||||||
kind: 'email',
|
kind: 'email',
|
||||||
app_id: "m.email",
|
app_id: 'm.email',
|
||||||
pushkey: address,
|
pushkey: address,
|
||||||
app_display_name: 'Email Notifications',
|
app_display_name: 'Email Notifications',
|
||||||
device_display_name: address,
|
device_display_name: address,
|
||||||
|
@ -121,46 +121,46 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getUrlPreviewsDisabled: function() {
|
getUrlPreviewsDisabled: function() {
|
||||||
var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
|
const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
|
||||||
return (event && event.getContent().disable);
|
return (event && event.getContent().disable);
|
||||||
},
|
},
|
||||||
|
|
||||||
setUrlPreviewsDisabled: function(disabled) {
|
setUrlPreviewsDisabled: function(disabled) {
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
|
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
|
||||||
disable: disabled
|
disable: disabled,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getSyncedSettings: function() {
|
getSyncedSettings: function() {
|
||||||
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
|
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
|
||||||
return event ? event.getContent() : {};
|
return event ? event.getContent() : {};
|
||||||
},
|
},
|
||||||
|
|
||||||
getSyncedSetting: function(type, defaultValue = null) {
|
getSyncedSetting: function(type, defaultValue = null) {
|
||||||
var settings = this.getSyncedSettings();
|
const settings = this.getSyncedSettings();
|
||||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
setSyncedSetting: function(type, value) {
|
setSyncedSetting: function(type, value) {
|
||||||
var settings = this.getSyncedSettings();
|
const settings = this.getSyncedSettings();
|
||||||
settings[type] = value;
|
settings[type] = value;
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
|
||||||
},
|
},
|
||||||
|
|
||||||
getLocalSettings: function() {
|
getLocalSettings: function() {
|
||||||
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||||
return JSON.parse(localSettingsString);
|
return JSON.parse(localSettingsString);
|
||||||
},
|
},
|
||||||
|
|
||||||
getLocalSetting: function(type, defaultValue = null) {
|
getLocalSetting: function(type, defaultValue = null) {
|
||||||
var settings = this.getLocalSettings();
|
const settings = this.getLocalSettings();
|
||||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
setLocalSetting: function(type, value) {
|
setLocalSetting: function(type, value) {
|
||||||
var settings = this.getLocalSettings();
|
const settings = this.getLocalSettings();
|
||||||
settings[type] = value;
|
settings[type] = value;
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||||
|
@ -171,8 +171,8 @@ module.exports = {
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
if (MatrixClientPeg.get().isGuest()) return false;
|
||||||
|
|
||||||
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
||||||
for (var i = 0; i < this.LABS_FEATURES.length; i++) {
|
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||||
var f = this.LABS_FEATURES[i];
|
const f = this.LABS_FEATURES[i];
|
||||||
if (f.id === feature) {
|
if (f.id === feature) {
|
||||||
return f.default;
|
return f.default;
|
||||||
}
|
}
|
||||||
|
@ -183,5 +183,5 @@ module.exports = {
|
||||||
|
|
||||||
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
||||||
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -99,26 +103,40 @@ import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/Unknow
|
||||||
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
||||||
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||||
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||||
|
import views$elements$ActionButton from './components/views/elements/ActionButton';
|
||||||
|
views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton);
|
||||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||||
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
||||||
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
||||||
views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
|
views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
|
||||||
|
import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton';
|
||||||
|
views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton);
|
||||||
import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
|
import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
|
||||||
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';
|
||||||
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
|
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
|
||||||
import views$elements$EmojiText from './components/views/elements/EmojiText';
|
import views$elements$EmojiText from './components/views/elements/EmojiText';
|
||||||
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
|
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
|
||||||
|
import views$elements$HomeButton from './components/views/elements/HomeButton';
|
||||||
|
views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton);
|
||||||
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
|
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
|
||||||
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
|
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
|
||||||
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
|
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
|
||||||
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
|
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
|
||||||
import views$elements$ProgressBar from './components/views/elements/ProgressBar';
|
import views$elements$ProgressBar from './components/views/elements/ProgressBar';
|
||||||
views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
|
views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
|
||||||
|
import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton';
|
||||||
|
views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton);
|
||||||
|
import views$elements$SettingsButton from './components/views/elements/SettingsButton';
|
||||||
|
views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton);
|
||||||
|
import views$elements$StartChatButton from './components/views/elements/StartChatButton';
|
||||||
|
views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton);
|
||||||
import views$elements$TintableSvg from './components/views/elements/TintableSvg';
|
import views$elements$TintableSvg from './components/views/elements/TintableSvg';
|
||||||
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
|
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
|
||||||
import views$elements$TruncatedList from './components/views/elements/TruncatedList';
|
import views$elements$TruncatedList from './components/views/elements/TruncatedList';
|
||||||
|
@ -129,6 +147,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 +241,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';
|
||||||
|
|
|
@ -140,13 +140,20 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -81,6 +82,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
|
||||||
|
@ -99,9 +107,21 @@ 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 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||||
var action = ev.keyCode == KeyCode.UP ?
|
var action = ev.keyCode == KeyCode.UP ?
|
||||||
'view_prev_room' : 'view_next_room';
|
'view_prev_room' : 'view_next_room';
|
||||||
dis.dispatch({action: action});
|
dis.dispatch({action: action});
|
||||||
|
@ -111,13 +131,15 @@ export default React.createClass({
|
||||||
|
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
this._onScrollKeyPressed(ev);
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
handled = true;
|
this._onScrollKeyPressed(ev);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.HOME:
|
case KeyCode.HOME:
|
||||||
case KeyCode.END:
|
case KeyCode.END:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this._onScrollKeyPressed(ev);
|
this._onScrollKeyPressed(ev);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
@ -135,22 +157,25 @@ export default React.createClass({
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
this.refs.roomView.handleScrollKey(ev);
|
this.refs.roomView.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
|
else if (this.refs.roomDirectory) {
|
||||||
|
this.refs.roomDirectory.handleScrollKey(ev);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
var RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
var RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
const UserSettings = sdk.getComponent('structures.UserSettings');
|
||||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
var HomePage = sdk.getComponent('structures.HomePage');
|
const HomePage = sdk.getComponent('structures.HomePage');
|
||||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||||
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
||||||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||||
|
|
||||||
var page_element;
|
let page_element;
|
||||||
var right_panel = '';
|
let right_panel = '';
|
||||||
|
|
||||||
switch (this.props.page_type) {
|
switch (this.props.page_type) {
|
||||||
case PageTypes.RoomView:
|
case PageTypes.RoomView:
|
||||||
|
@ -195,10 +220,9 @@ export default React.createClass({
|
||||||
|
|
||||||
case PageTypes.RoomDirectory:
|
case PageTypes.RoomDirectory:
|
||||||
page_element = <RoomDirectory
|
page_element = <RoomDirectory
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
ref="roomDirectory"
|
||||||
config={this.props.config.roomDirectory}
|
config={this.props.config.roomDirectory}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.HomePage:
|
case PageTypes.HomePage:
|
||||||
|
|
|
@ -29,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');
|
||||||
|
@ -63,6 +59,13 @@ 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,
|
||||||
|
@ -89,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,
|
||||||
|
@ -104,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,
|
||||||
|
@ -184,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;
|
||||||
|
@ -280,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.
|
||||||
|
@ -307,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,
|
||||||
|
@ -317,14 +322,13 @@ 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':
|
||||||
|
@ -344,14 +348,20 @@ module.exports = React.createClass({
|
||||||
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;
|
||||||
|
@ -359,8 +369,8 @@ module.exports = React.createClass({
|
||||||
// also stash our credentials, then if we restore the session,
|
// also stash our credentials, then if we restore the session,
|
||||||
// we can just do it the same way whether we started upgrade
|
// we can just do it the same way whether we started upgrade
|
||||||
// registration or explicitly logged out
|
// registration or explicitly logged out
|
||||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
|
||||||
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(),
|
||||||
|
@ -375,35 +385,60 @@ module.exports = React.createClass({
|
||||||
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: (err && err.message ? err.message : "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()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -530,6 +565,9 @@ 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(payload.teamToken);
|
this._onLoggedIn(payload.teamToken);
|
||||||
break;
|
break;
|
||||||
|
@ -603,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() {
|
||||||
|
@ -658,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();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -708,18 +756,46 @@ module.exports = React.createClass({
|
||||||
* Called when a new logged in session has started
|
* Called when a new logged in session has started
|
||||||
*/
|
*/
|
||||||
_onLoggedIn: function(teamToken) {
|
_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) {
|
if (teamToken) {
|
||||||
|
// A team member has logged in, not a guest
|
||||||
this._teamToken = teamToken;
|
this._teamToken = teamToken;
|
||||||
this._setPage(PageTypes.HomePage);
|
dis.dispatch({action: 'view_home_page'});
|
||||||
} else if (this._is_registered) {
|
} else if (this._is_registered) {
|
||||||
this._setPage(PageTypes.UserSettings);
|
// 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'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -729,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,
|
||||||
|
@ -745,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") {
|
||||||
|
@ -755,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({
|
||||||
|
@ -903,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) {
|
||||||
|
@ -1002,9 +1052,9 @@ 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});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1086,14 +1136,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
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 />
|
||||||
|
@ -1102,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}
|
||||||
|
@ -1121,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 />
|
||||||
|
@ -1133,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}
|
||||||
|
@ -1153,10 +1204,11 @@ module.exports = React.createClass({
|
||||||
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()}
|
||||||
|
@ -1168,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}
|
||||||
|
@ -1180,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -279,23 +279,25 @@ module.exports = React.createClass({
|
||||||
this.currentGhostEventId = null;
|
this.currentGhostEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isMembershipChange = (e) =>
|
var isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||||
e.getType() === 'm.room.member'
|
|
||||||
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
var mxEv = this.props.events[i];
|
let mxEv = this.props.events[i];
|
||||||
var wantTile = true;
|
let wantTile = true;
|
||||||
var eventId = mxEv.getId();
|
let eventId = mxEv.getId();
|
||||||
|
let readMarkerInMels = false;
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
wantTile = false;
|
wantTile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var last = (i == lastShownEventIndex);
|
let last = (i == lastShownEventIndex);
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) && 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
|
||||||
|
@ -331,6 +333,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let eventTiles = summarisedEvents.map(
|
let eventTiles = summarisedEvents.map(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (e.getId() === this.props.readMarkerEventId) {
|
||||||
|
readMarkerInMels = true;
|
||||||
|
}
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
@ -349,12 +354,16 @@ module.exports = React.createClass({
|
||||||
<MemberEventListSummary
|
<MemberEventListSummary
|
||||||
key={key}
|
key={key}
|
||||||
events={summarisedEvents}
|
events={summarisedEvents}
|
||||||
data-scroll-token={eventId}
|
|
||||||
onToggle={this._onWidgetLoad} // Update scroll state
|
onToggle={this._onWidgetLoad} // Update scroll state
|
||||||
>
|
>
|
||||||
{eventTiles}
|
{eventTiles}
|
||||||
</MemberEventListSummary>
|
</MemberEventListSummary>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (readMarkerInMels) {
|
||||||
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,6 +394,8 @@ module.exports = React.createClass({
|
||||||
isVisibleReadMarker = visible;
|
isVisibleReadMarker = visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: there should be no need for a ghost tile - we should just use a
|
||||||
|
// a dispatch (user_activity_end) to start the RM animation.
|
||||||
if (eventId == this.currentGhostEventId) {
|
if (eventId == this.currentGhostEventId) {
|
||||||
// if we're showing an animation, continue to show it.
|
// if we're showing an animation, continue to show it.
|
||||||
ret.push(this._getReadMarkerGhostTile());
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
@ -408,7 +419,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;
|
||||||
|
@ -459,8 +472,9 @@ module.exports = React.createClass({
|
||||||
ret.push(
|
ret.push(
|
||||||
<li key={eventId}
|
<li key={eventId}
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
data-scroll-token={scrollToken}>
|
data-scroll-tokens={scrollToken}>
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
|
isRedacted={mxEv.isRedacted()}
|
||||||
onWidgetLoad={this._onWidgetLoad}
|
onWidgetLoad={this._onWidgetLoad}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
readReceiptMap={this._readReceiptMap}
|
readReceiptMap={this._readReceiptMap}
|
||||||
|
@ -481,13 +495,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,33 +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 ||
|
||||||
props.tabComplete.isTabCompleting()
|
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
|
||||||
|
|
|
@ -26,6 +26,7 @@ var q = require("q");
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
|
||||||
|
var UserSettingsStore = require('../../UserSettingsStore');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var ContentMessages = require("../../ContentMessages");
|
var ContentMessages = require("../../ContentMessages");
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
|
@ -270,6 +271,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this._updateConfCallNotification();
|
this._updateConfCallNotification();
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.onPageUnload);
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
this.onResize();
|
this.onResize();
|
||||||
|
|
||||||
|
@ -352,6 +354,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||||
window.removeEventListener('resize', this.onResize);
|
window.removeEventListener('resize', this.onResize);
|
||||||
|
|
||||||
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keydown", this.onKeyDown);
|
||||||
|
@ -364,6 +367,17 @@ module.exports = React.createClass({
|
||||||
// Tinter.tint(); // reset colourscheme
|
// Tinter.tint(); // reset colourscheme
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPageUnload(event) {
|
||||||
|
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||||
|
return event.returnValue =
|
||||||
|
'You seem to be uploading files, are you sure you want to quit?';
|
||||||
|
} else if (this._getCallForRoom() && this.state.callState !== 'ended') {
|
||||||
|
return event.returnValue =
|
||||||
|
'You seem to be in a call, are you sure you want to quit?';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown: function(ev) {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
@ -489,6 +503,13 @@ 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) {
|
||||||
|
@ -914,8 +935,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, {
|
||||||
|
@ -927,11 +946,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: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1015,9 +1043,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: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("),
|
||||||
});
|
});
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -1165,6 +1194,7 @@ module.exports = React.createClass({
|
||||||
console.log("updateTint from onCancelClick");
|
console.log("updateTint from onCancelClick");
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
this.setState({editingRoomSettings: false});
|
this.setState({editingRoomSettings: false});
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
onLeaveClick: function() {
|
onLeaveClick: function() {
|
||||||
|
@ -1238,6 +1268,7 @@ module.exports = React.createClass({
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
jumpToLiveTimeline: function() {
|
jumpToLiveTimeline: function() {
|
||||||
this.refs.messagePanel.jumpToLiveTimeline();
|
this.refs.messagePanel.jumpToLiveTimeline();
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1257,12 +1288,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pos = this.refs.messagePanel.getReadMarkerPosition();
|
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
||||||
|
|
||||||
// we want to show the bar if the read-marker is off the top of the
|
|
||||||
// screen.
|
|
||||||
var showBar = (pos < 0);
|
|
||||||
|
|
||||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||||
this.setState({showTopUnreadMessagesBar: showBar},
|
this.setState({showTopUnreadMessagesBar: showBar},
|
||||||
this.onChildResize);
|
this.onChildResize);
|
||||||
|
@ -1701,7 +1727,7 @@ module.exports = React.createClass({
|
||||||
var messagePanel = (
|
var messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
manageReadReceipts={true}
|
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||||
manageReadMarkers={true}
|
manageReadMarkers={true}
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
highlightedEventId={this.props.highlightedEventId}
|
highlightedEventId={this.props.highlightedEventId}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
|
||||||
* It also provides a hook which allows parents to provide more list elements
|
* It also provides a hook which allows parents to provide more list elements
|
||||||
* when we get close to the start or end of the list.
|
* when we get close to the start or end of the list.
|
||||||
*
|
*
|
||||||
* Each child element should have a 'data-scroll-token'. This token is used to
|
* Each child element should have a 'data-scroll-tokens'. This string of
|
||||||
* serialise the scroll state, and returned as the 'trackedScrollToken'
|
* comma-separated tokens may contain a single token or many, where many indicates
|
||||||
* attribute by getScrollState().
|
* that the element contains elements that have scroll tokens themselves. The first
|
||||||
|
* token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
|
||||||
|
* as the 'trackedScrollToken' attribute by getScrollState().
|
||||||
|
*
|
||||||
|
* IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
|
||||||
*
|
*
|
||||||
* Some notes about the implementation:
|
* Some notes about the implementation:
|
||||||
*
|
*
|
||||||
|
@ -333,33 +337,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.scrollTokens) {
|
||||||
|
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
|
||||||
}
|
}
|
||||||
} 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--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,7 +423,8 @@ module.exports = React.createClass({
|
||||||
* scroll. false if we are tracking a particular child.
|
* scroll. false if we are tracking a particular child.
|
||||||
*
|
*
|
||||||
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
|
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
|
||||||
* false, the data-scroll-token of the child which we are tracking.
|
* false, the first token in data-scroll-tokens of the child which we are
|
||||||
|
* tracking.
|
||||||
*
|
*
|
||||||
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
|
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
|
||||||
* the number of pixels the bottom of the tracked child is above the
|
* the number of pixels the bottom of the tracked child is above the
|
||||||
|
@ -489,21 +488,25 @@ module.exports = React.createClass({
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
this.scrollRelative(-1);
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
|
this.scrollRelative(-1);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
this.scrollRelative(1);
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
|
this.scrollRelative(1);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.HOME:
|
case KeyCode.HOME:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.END:
|
case KeyCode.END:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -553,8 +556,10 @@ module.exports = React.createClass({
|
||||||
var messages = this.refs.itemlist.children;
|
var messages = this.refs.itemlist.children;
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (var i = messages.length-1; i >= 0; --i) {
|
||||||
var m = messages[i];
|
var m = messages[i];
|
||||||
if (!m.dataset.scrollToken) continue;
|
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||||
if (m.dataset.scrollToken == scrollToken) {
|
// There might only be one scroll token
|
||||||
|
if (m.dataset.scrollTokens &&
|
||||||
|
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
|
||||||
node = m;
|
node = m;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -570,7 +575,7 @@ module.exports = React.createClass({
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||||
|
|
||||||
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
|
@ -589,24 +594,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.scrollTokens) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
newScrollState = {
|
||||||
this.scrollState = {
|
stuckAtBottom: false,
|
||||||
stuckAtBottom: false,
|
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||||
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.scrollTokens` 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() {
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -102,9 +103,6 @@ var TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
// a map from room id to read marker event ID
|
|
||||||
roomReadMarkerMap: {},
|
|
||||||
|
|
||||||
// a map from room id to read marker event timestamp
|
// a map from room id to read marker event timestamp
|
||||||
roomReadMarkerTsMap: {},
|
roomReadMarkerTsMap: {},
|
||||||
},
|
},
|
||||||
|
@ -121,10 +119,14 @@ var TimelinePanel = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||||
// but for now we just do it per room for simplicity.
|
// but for now we just do it per room for simplicity.
|
||||||
|
let initialReadMarker = null;
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
var initialReadMarker =
|
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||||
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|
if (readmarker){
|
||||||
|| this._getCurrentReadReceipt();
|
initialReadMarker = readmarker.getContent().event_id;
|
||||||
|
} else {
|
||||||
|
initialReadMarker = this._getCurrentReadReceipt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -166,6 +168,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
backPaginating: false,
|
backPaginating: false,
|
||||||
forwardPaginating: false,
|
forwardPaginating: false,
|
||||||
|
|
||||||
|
// cache of matrixClient.getSyncState() (but from the 'sync' event)
|
||||||
|
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -173,6 +178,7 @@ var TimelinePanel = React.createClass({
|
||||||
debuglog("TimelinePanel: mounting");
|
debuglog("TimelinePanel: mounting");
|
||||||
|
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
this.last_rm_sent_event_id = undefined;
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
|
@ -180,6 +186,8 @@ var TimelinePanel = React.createClass({
|
||||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
|
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||||
|
MatrixClientPeg.get().on("sync", this.onSync);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
},
|
},
|
||||||
|
@ -247,14 +255,18 @@ var TimelinePanel = React.createClass({
|
||||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
|
client.removeListener("Room.accountData", this.onAccountData);
|
||||||
|
client.removeListener("sync", this.onSync);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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(
|
||||||
|
@ -412,6 +424,7 @@ var TimelinePanel = React.createClass({
|
||||||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||||
// we know we're stuckAtBottom, so we can advance the RM
|
// we know we're stuckAtBottom, so we can advance the RM
|
||||||
// immediately, to save a later render cycle
|
// immediately, to save a later render cycle
|
||||||
|
|
||||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||||
updatedState.readMarkerVisible = false;
|
updatedState.readMarkerVisible = false;
|
||||||
updatedState.readMarkerEventId = lastEv.getId();
|
updatedState.readMarkerEventId = lastEv.getId();
|
||||||
|
@ -431,6 +444,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;
|
||||||
|
|
||||||
|
@ -460,6 +477,25 @@ var TimelinePanel = React.createClass({
|
||||||
this._reloadEvents();
|
this._reloadEvents();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAccountData: function(ev, room) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
// ignore events for other rooms
|
||||||
|
if (room !== this.props.timelineSet.room) return;
|
||||||
|
|
||||||
|
if (ev.getType() !== "m.fully_read") return;
|
||||||
|
|
||||||
|
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
|
||||||
|
// this mechanism of determining where the RM is relative to the view-port with
|
||||||
|
// one supported by the server (the client needs more than an event ID).
|
||||||
|
this.setState({
|
||||||
|
readMarkerEventId: ev.getContent().event_id,
|
||||||
|
}, this.props.onReadMarkerUpdated);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSync: function(state, prevState, data) {
|
||||||
|
this.setState({clientSyncState: state});
|
||||||
|
},
|
||||||
|
|
||||||
sendReadReceipt: function() {
|
sendReadReceipt: function() {
|
||||||
if (!this.refs.messagePanel) return;
|
if (!this.refs.messagePanel) return;
|
||||||
|
@ -467,15 +503,9 @@ var TimelinePanel = React.createClass({
|
||||||
// This happens on user_activity_end which is delayed, and it's
|
// This happens on user_activity_end which is delayed, and it's
|
||||||
// very possible have logged out within that timeframe, so check
|
// very possible have logged out within that timeframe, so check
|
||||||
// we still have a client.
|
// we still have a client.
|
||||||
if (!MatrixClientPeg.get()) return;
|
const cli = MatrixClientPeg.get();
|
||||||
|
// if no client or client is guest don't send RR
|
||||||
// if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
|
if (!cli || cli.isGuest()) return;
|
||||||
// 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);
|
||||||
|
@ -507,13 +537,44 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// we also remember the last read receipt we sent to avoid spamming the
|
// we also remember the last read receipt we sent to avoid spamming the
|
||||||
// same one at the server repeatedly
|
// same one at the server repeatedly
|
||||||
if (lastReadEventIndex > currentReadUpToEventIndex
|
if ((lastReadEventIndex > currentReadUpToEventIndex &&
|
||||||
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
this.last_rr_sent_event_id != lastReadEvent.getId()) ||
|
||||||
|
this.last_rm_sent_event_id != this.state.readMarkerEventId) {
|
||||||
|
|
||||||
this.last_rr_sent_event_id = lastReadEvent.getId();
|
this.last_rr_sent_event_id = lastReadEvent.getId();
|
||||||
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
this.last_rm_sent_event_id = this.state.readMarkerEventId;
|
||||||
|
|
||||||
|
MatrixClientPeg.get().setRoomReadMarkers(
|
||||||
|
this.props.timelineSet.room.roomId,
|
||||||
|
this.state.readMarkerEventId,
|
||||||
|
lastReadEvent
|
||||||
|
).catch((e) => {
|
||||||
|
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||||
|
if (e.errcode === 'M_UNRECOGNIZED') {
|
||||||
|
return MatrixClientPeg.get().sendReadReceipt(
|
||||||
|
lastReadEvent
|
||||||
|
).catch(() => {
|
||||||
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
// it failed, so allow retries next time the user is active
|
// it failed, so allow retries next time the user is active
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
this.last_rm_sent_event_id = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// do a quick-reset of our unreadNotificationCount to avoid having
|
||||||
|
// 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',
|
||||||
|
room: this.props.timelineSet.room,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -695,7 +756,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// the messagePanel doesn't know where the read marker is.
|
// the messagePanel doesn't know where the read marker is.
|
||||||
// if we know the timestamp of the read marker, make a guess based on that.
|
// if we know the timestamp of the read marker, make a guess based on that.
|
||||||
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
|
const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
|
||||||
if (rmTs && this.state.events.length > 0) {
|
if (rmTs && this.state.events.length > 0) {
|
||||||
if (rmTs < this.state.events[0].getTs()) {
|
if (rmTs < this.state.events[0].getTs()) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -707,6 +768,19 @@ var TimelinePanel = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canJumpToReadMarker: function() {
|
||||||
|
// 1. Do not show jump bar if neither the RM nor the RR are set.
|
||||||
|
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
|
||||||
|
// read messages and unread messages. We already have a badge count and the bottom
|
||||||
|
// bar to jump to "live" when we have unread messages.
|
||||||
|
// 3. We want to show the bar if the read-marker is off the top of the screen.
|
||||||
|
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
|
||||||
|
const pos = this.getReadMarkerPosition();
|
||||||
|
return this.state.readMarkerEventId !== null && // 1.
|
||||||
|
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
|
||||||
|
(pos < 0 || pos === null); // 3., 4.
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* called by the parent component when PageUp/Down/etc is pressed.
|
* called by the parent component when PageUp/Down/etc is pressed.
|
||||||
*
|
*
|
||||||
|
@ -717,7 +791,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||||
// timeline window.
|
// timeline window.
|
||||||
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||||
|
ev.keyCode == KeyCode.END)
|
||||||
|
{
|
||||||
this.jumpToLiveTimeline();
|
this.jumpToLiveTimeline();
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.handleScrollKey(ev);
|
this.refs.messagePanel.handleScrollKey(ev);
|
||||||
|
@ -810,7 +886,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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -945,16 +1021,12 @@ var TimelinePanel = React.createClass({
|
||||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||||
var roomId = this.props.timelineSet.room.roomId;
|
var roomId = this.props.timelineSet.room.roomId;
|
||||||
|
|
||||||
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
|
// don't update the state (and cause a re-render) if there is
|
||||||
// don't update the state (and cause a re-render) if there is
|
// no change to the RM.
|
||||||
// no change to the RM.
|
if (eventId === this.state.readMarkerEventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ideally we'd sync these via the server, but for now just stash them
|
|
||||||
// in a map.
|
|
||||||
TimelinePanel.roomReadMarkerMap[roomId] = eventId;
|
|
||||||
|
|
||||||
// in order to later figure out if the read marker is
|
// in order to later figure out if the read marker is
|
||||||
// above or below the visible timeline, we stash the timestamp.
|
// above or below the visible timeline, we stash the timestamp.
|
||||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||||
|
@ -963,6 +1035,7 @@ var TimelinePanel = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do the local echo of the RM
|
||||||
// run the render cycle before calling the callback, so that
|
// run the render cycle before calling the callback, so that
|
||||||
// getReadMarkerPosition() returns the right thing.
|
// getReadMarkerPosition() returns the right thing.
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1011,11 +1084,17 @@ var TimelinePanel = React.createClass({
|
||||||
// of paginating our way through the entire history of the room.
|
// of paginating our way through the entire history of the room.
|
||||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||||
|
|
||||||
|
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
||||||
|
// the HS and fetch the latest events, so we are effectively forward paginating.
|
||||||
|
const forwardPaginating = (
|
||||||
|
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagePanel ref="messagePanel"
|
<MessagePanel ref="messagePanel"
|
||||||
hidden={ this.props.hidden }
|
hidden={ this.props.hidden }
|
||||||
backPaginating={ this.state.backPaginating }
|
backPaginating={ this.state.backPaginating }
|
||||||
forwardPaginating={ this.state.forwardPaginating }
|
forwardPaginating={ forwardPaginating }
|
||||||
events={ this.state.events }
|
events={ this.state.events }
|
||||||
highlightedEventId={ this.props.highlightedEventId }
|
highlightedEventId={ this.props.highlightedEventId }
|
||||||
readMarkerEventId={ this.state.readMarkerEventId }
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -13,27 +14,40 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
var sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var PlatformPeg = require("../../PlatformPeg");
|
const PlatformPeg = require("../../PlatformPeg");
|
||||||
var Modal = require('../../Modal');
|
const Modal = require('../../Modal');
|
||||||
var dis = require("../../dispatcher");
|
const dis = require("../../dispatcher");
|
||||||
var q = require('q');
|
const q = require('q');
|
||||||
var package_json = require('../../../package.json');
|
const packageJson = require('../../../package.json');
|
||||||
var UserSettingsStore = require('../../UserSettingsStore');
|
const UserSettingsStore = require('../../UserSettingsStore');
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var Email = require('../../email');
|
const Email = require('../../email');
|
||||||
var AddThreepid = require('../../AddThreepid');
|
const AddThreepid = require('../../AddThreepid');
|
||||||
var SdkConfig = require('../../SdkConfig');
|
const SdkConfig = require('../../SdkConfig');
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
|
||||||
// if this looks like a release, use the 'version' from package.json; else use
|
// if this looks like a release, use the 'version' from package.json; else use
|
||||||
// the git sha.
|
// the git sha. Prepend version with v, to look like riot-web version
|
||||||
const REACT_SDK_VERSION =
|
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
|
||||||
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
|
|
||||||
|
|
||||||
|
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
||||||
|
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
||||||
|
const gHVersionLabel = function(repo, token) {
|
||||||
|
const match = token.match(semVerRegex);
|
||||||
|
let url;
|
||||||
|
if (match && match[1]) { // basic semVer string possibly with commit hash
|
||||||
|
url = (match.length > 1 && match[2])
|
||||||
|
? `https://github.com/${repo}/commit/${match[2]}`
|
||||||
|
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
|
||||||
|
} else {
|
||||||
|
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||||
|
}
|
||||||
|
return <a href={url}>{token}</a>;
|
||||||
|
};
|
||||||
|
|
||||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||||
|
@ -43,6 +57,14 @@ const SETTINGS_LABELS = [
|
||||||
id: 'autoplayGifsAndVideos',
|
id: 'autoplayGifsAndVideos',
|
||||||
label: 'Autoplay GIFs and videos',
|
label: 'Autoplay GIFs and videos',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'hideReadReceipts',
|
||||||
|
label: 'Hide read receipts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dontSendTypingNotifications',
|
||||||
|
label: "Don't send typing notifications",
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
id: 'alwaysShowTimestamps',
|
id: 'alwaysShowTimestamps',
|
||||||
|
@ -93,7 +115,7 @@ const THEMES = [
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
label: 'Dark theme',
|
label: 'Dark theme',
|
||||||
value: 'dark',
|
value: 'dark',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,6 +161,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(() => {
|
||||||
|
@ -166,7 +189,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
this._refreshFromServer();
|
this._refreshFromServer();
|
||||||
|
|
||||||
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
if (!syncedSettings.theme) {
|
if (!syncedSettings.theme) {
|
||||||
syncedSettings.theme = 'light';
|
syncedSettings.theme = 'light';
|
||||||
}
|
}
|
||||||
|
@ -188,16 +211,16 @@ module.exports = React.createClass({
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
});
|
});
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
let cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshFromServer: function() {
|
_refreshFromServer: function() {
|
||||||
var self = this;
|
const self = this;
|
||||||
q.all([
|
q.all([
|
||||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
|
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
|
||||||
]).done(function(resps) {
|
]).done(function(resps) {
|
||||||
self.setState({
|
self.setState({
|
||||||
avatarUrl: resps[0].avatar_url,
|
avatarUrl: resps[0].avatar_url,
|
||||||
|
@ -205,10 +228,11 @@ module.exports = React.createClass({
|
||||||
phase: "UserSettings.DISPLAY",
|
phase: "UserSettings.DISPLAY",
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to load user settings: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Can't load user settings",
|
title: "Can't load user settings",
|
||||||
description: error.toString()
|
description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -221,7 +245,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onAvatarPickerClick: function(ev) {
|
onAvatarPickerClick: function(ev) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||||
Modal.createDialog(NeedToRegisterDialog, {
|
Modal.createDialog(NeedToRegisterDialog, {
|
||||||
title: "Please Register",
|
title: "Please Register",
|
||||||
description: "Guests can't set avatars. Please register.",
|
description: "Guests can't set avatars. Please register.",
|
||||||
|
@ -235,8 +259,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAvatarSelected: function(ev) {
|
onAvatarSelected: function(ev) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var changeAvatar = this.refs.changeAvatar;
|
const changeAvatar = this.refs.changeAvatar;
|
||||||
if (!changeAvatar) {
|
if (!changeAvatar) {
|
||||||
console.error("No ChangeAvatar found to upload image to!");
|
console.error("No ChangeAvatar found to upload image to!");
|
||||||
return;
|
return;
|
||||||
|
@ -245,27 +269,34 @@ module.exports = React.createClass({
|
||||||
// dunno if the avatar changed, re-check it.
|
// dunno if the avatar changed, re-check it.
|
||||||
self._refreshFromServer();
|
self._refreshFromServer();
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
console.error("Failed to set avatar: " + err);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Error",
|
title: "Failed to set avatar",
|
||||||
description: "Failed to set avatar. " + errMsg
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Sign out?",
|
title: "Sign out?",
|
||||||
description:
|
description:
|
||||||
<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 key="export" 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'});
|
||||||
|
@ -278,33 +309,33 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordChangeError: function(err) {
|
onPasswordChangeError: function(err) {
|
||||||
var errMsg = err.error || "";
|
let errMsg = err.error || "";
|
||||||
if (err.httpStatus === 403) {
|
if (err.httpStatus === 403) {
|
||||||
errMsg = "Failed to change password. Is your password correct?";
|
errMsg = "Failed to change password. Is your password correct?";
|
||||||
}
|
} else if (err.httpStatus) {
|
||||||
else if (err.httpStatus) {
|
|
||||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to change password: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: errMsg
|
description: errMsg,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordChanged: function() {
|
onPasswordChanged: function() {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: `Your password was successfully changed. You will not
|
description: `Your password was successfully changed. You will not
|
||||||
receive push notifications on other devices until you
|
receive push notifications on other devices until you
|
||||||
log back in to them.`
|
log back in to them.`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onUpgradeClicked: function() {
|
onUpgradeClicked: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "start_upgrade_registration"
|
action: "start_upgrade_registration",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -312,23 +343,27 @@ 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;
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
this._addEmail();
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
},
|
||||||
|
|
||||||
var email_address = this.refs.add_threepid_input.value;
|
_addEmail: function() {
|
||||||
if (!Email.looksValid(email_address)) {
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
|
||||||
|
const emailAddress = this.refs.add_email_input.value;
|
||||||
|
if (!Email.looksValid(emailAddress)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Invalid Email Address",
|
title: "Invalid Email Address",
|
||||||
description: "This doesn't appear to be a valid email address",
|
description: "This doesn't appear to be a valid email address",
|
||||||
});
|
});
|
||||||
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(emailAddress, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Verification Pending",
|
title: "Verification Pending",
|
||||||
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||||
|
@ -337,12 +372,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 " + emailAddress + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to add email address",
|
title: "Unable to add email address",
|
||||||
description: err.message
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -361,9 +397,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: "Unable to remove contact information",
|
||||||
description: err.toString(),
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
|
@ -380,8 +417,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",
|
||||||
});
|
});
|
||||||
|
@ -389,9 +426,9 @@ module.exports = React.createClass({
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var message = "Unable to verify email address. ";
|
let message = "Unable to verify email address. ";
|
||||||
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
|
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Verification Pending",
|
title: "Verification Pending",
|
||||||
|
@ -400,10 +437,11 @@ module.exports = React.createClass({
|
||||||
onFinished: this.onEmailDialogFinished,
|
onFinished: this.onEmailDialogFinished,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to verify email address",
|
title: "Unable to verify email address",
|
||||||
description: err.toString(),
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -423,10 +461,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);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -438,17 +477,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onRejectAllInvitesClicked: function(rooms, ev) {
|
_onRejectAllInvitesClicked: function(rooms, ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
rejectingInvites: true
|
rejectingInvites: true,
|
||||||
});
|
});
|
||||||
// reject the invites
|
// reject the invites
|
||||||
let promises = rooms.map((room) => {
|
const promises = rooms.map((room) => {
|
||||||
return MatrixClientPeg.get().leave(room.roomId);
|
return MatrixClientPeg.get().leave(room.roomId);
|
||||||
});
|
});
|
||||||
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||||
// after trying to reject all the invites.
|
// after trying to reject all the invites.
|
||||||
q.allSettled(promises).then(() => {
|
q.allSettled(promises).then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
rejectingInvites: false
|
rejectingInvites: false,
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -461,7 +500,7 @@ module.exports = React.createClass({
|
||||||
}, "e2e-export");
|
}, "e2e-export");
|
||||||
}, {
|
}, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -473,7 +512,7 @@ module.exports = React.createClass({
|
||||||
}, "e2e-export");
|
}, "e2e-export");
|
||||||
}, {
|
}, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -499,8 +538,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_renderUserInterfaceSettings: function() {
|
||||||
var client = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>User Interface</h3>
|
<h3>User Interface</h3>
|
||||||
|
@ -527,7 +564,7 @@ module.exports = React.createClass({
|
||||||
<input id="urlPreviewsDisabled"
|
<input id="urlPreviewsDisabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||||
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
||||||
/>
|
/>
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
Disable inline URL previews by default
|
Disable inline URL previews by default
|
||||||
|
@ -540,7 +577,7 @@ module.exports = React.createClass({
|
||||||
<input id={ setting.id }
|
<input id={ setting.id }
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._syncedSettings[setting.id] }
|
defaultChecked={ this._syncedSettings[setting.id] }
|
||||||
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={ setting.id }>
|
||||||
{ setting.label }
|
{ setting.label }
|
||||||
|
@ -555,7 +592,7 @@ module.exports = React.createClass({
|
||||||
name={ setting.id }
|
name={ setting.id }
|
||||||
value={ setting.value }
|
value={ setting.value }
|
||||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||||
onChange={ e => {
|
onChange={ (e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||||
}
|
}
|
||||||
|
@ -617,8 +654,8 @@ module.exports = React.createClass({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._localSettings[setting.id] }
|
defaultChecked={ this._localSettings[setting.id] }
|
||||||
onChange={
|
onChange={
|
||||||
e => {
|
(e) => {
|
||||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
|
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
|
||||||
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
|
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
|
||||||
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
|
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
|
||||||
}
|
}
|
||||||
|
@ -632,7 +669,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDevicesPanel: function() {
|
_renderDevicesPanel: function() {
|
||||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Devices</h3>
|
<h3>Devices</h3>
|
||||||
|
@ -643,7 +680,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_renderBugReport: function() {
|
_renderBugReport: function() {
|
||||||
if (!SdkConfig.get().bug_report_endpoint_url) {
|
if (!SdkConfig.get().bug_report_endpoint_url) {
|
||||||
return <div />
|
return <div />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -662,17 +699,17 @@ module.exports = React.createClass({
|
||||||
// default to enabled if undefined
|
// default to enabled if undefined
|
||||||
if (this.props.enableLabs === false) return null;
|
if (this.props.enableLabs === false) return null;
|
||||||
|
|
||||||
let features = UserSettingsStore.LABS_FEATURES.map(feature => (
|
const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
|
||||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={feature.id}
|
id={feature.id}
|
||||||
name={feature.id}
|
name={feature.id}
|
||||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
e.target.checked = false;
|
e.target.checked = false;
|
||||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||||
Modal.createDialog(NeedToRegisterDialog, {
|
Modal.createDialog(NeedToRegisterDialog, {
|
||||||
title: "Please Register",
|
title: "Please Register",
|
||||||
description: "Guests can't use labs features. Please register.",
|
description: "Guests can't use labs features. Please register.",
|
||||||
|
@ -724,14 +761,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderBulkOptions: function() {
|
_renderBulkOptions: function() {
|
||||||
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||||
return r.hasMembershipState(this._me, "invite");
|
return r.hasMembershipState(this._me, "invite");
|
||||||
});
|
});
|
||||||
if (invitedRooms.length === 0) {
|
if (invitedRooms.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
let reject = <Spinner />;
|
let reject = <Spinner />;
|
||||||
if (!this.state.rejectingInvites) {
|
if (!this.state.rejectingInvites) {
|
||||||
|
@ -753,13 +790,33 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_showSpoiler: function(event) {
|
||||||
|
const target = event.target;
|
||||||
|
target.innerHTML = target.getAttribute('data-spoiler');
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(target);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
},
|
||||||
|
|
||||||
nameForMedium: function(medium) {
|
nameForMedium: function(medium) {
|
||||||
if (medium == 'msisdn') return 'Phone';
|
if (medium === 'msisdn') return 'Phone';
|
||||||
return medium[0].toUpperCase() + medium.slice(1);
|
return medium[0].toUpperCase() + medium.slice(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
presentableTextForThreepid: function(threepid) {
|
||||||
|
if (threepid.medium === 'msisdn') {
|
||||||
|
return '+' + threepid.address;
|
||||||
|
} else {
|
||||||
|
return threepid.address;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case "UserSettings.LOADING":
|
case "UserSettings.LOADING":
|
||||||
return (
|
return (
|
||||||
|
@ -771,18 +828,18 @@ module.exports = React.createClass({
|
||||||
throw new Error("Unknown state.phase => " + this.state.phase);
|
throw new Error("Unknown state.phase => " + this.state.phase);
|
||||||
}
|
}
|
||||||
// can only get here if phase is UserSettings.DISPLAY
|
// can only get here if phase is UserSettings.DISPLAY
|
||||||
var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||||
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||||
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
var Notifications = sdk.getComponent("settings.Notifications");
|
const Notifications = sdk.getComponent("settings.Notifications");
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
const EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
|
||||||
var avatarUrl = (
|
const avatarUrl = (
|
||||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
var threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
||||||
const id = "3pid-" + val.address;
|
const id = "3pid-" + val.address;
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||||
|
@ -790,7 +847,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)} />
|
||||||
|
@ -798,32 +857,37 @@ 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;
|
let accountJsx;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
accountJsx = (
|
accountJsx = (
|
||||||
|
@ -831,8 +895,7 @@ module.exports = React.createClass({
|
||||||
Create an account
|
Create an account
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
accountJsx = (
|
accountJsx = (
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
className="mx_UserSettings_accountTable"
|
className="mx_UserSettings_accountTable"
|
||||||
|
@ -844,9 +907,9 @@ module.exports = React.createClass({
|
||||||
onFinished={this.onPasswordChanged} />
|
onFinished={this.onPasswordChanged} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
var notification_area;
|
let notificationArea;
|
||||||
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
|
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
|
||||||
notification_area = (<div>
|
notificationArea = (<div>
|
||||||
<h3>Notifications</h3>
|
<h3>Notifications</h3>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
|
@ -855,12 +918,12 @@ module.exports = React.createClass({
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
var olmVersion = MatrixClientPeg.get().olmVersion;
|
const olmVersion = MatrixClientPeg.get().olmVersion;
|
||||||
// If the olmVersion is not defined then either crypto is disabled, or
|
// If the olmVersion is not defined then either crypto is disabled, or
|
||||||
// we are using a version old version of olm. We assume the former.
|
// we are using a version old version of olm. We assume the former.
|
||||||
var olmVersionString = "<not-enabled>";
|
let olmVersionString = "<not-enabled>";
|
||||||
if (olmVersion !== undefined) {
|
if (olmVersion !== undefined) {
|
||||||
olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2];
|
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -918,7 +981,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
{this._renderReferral()}
|
{this._renderReferral()}
|
||||||
|
|
||||||
{notification_area}
|
{notificationArea}
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{this._renderUserInterfaceSettings()}
|
||||||
{this._renderLabs()}
|
{this._renderLabs()}
|
||||||
|
@ -933,6 +996,12 @@ module.exports = React.createClass({
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
Logged in as {this._me}
|
Logged in as {this._me}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx_UserSettings_advanced">
|
||||||
|
Access Token: <span className="mx_UserSettings_advanced_spoiler"
|
||||||
|
onClick={this._showSpoiler}
|
||||||
|
data-spoiler={ MatrixClientPeg.get().getAccessToken() }
|
||||||
|
><click to reveal></span>
|
||||||
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
|
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
|
||||||
</div>
|
</div>
|
||||||
|
@ -940,8 +1009,14 @@ module.exports = React.createClass({
|
||||||
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
|
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
|
||||||
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
|
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||||
|
: REACT_SDK_VERSION
|
||||||
|
}<br/>
|
||||||
|
riot-web version: {(this.state.vectorVersion !== null)
|
||||||
|
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||||
|
: 'unknown'
|
||||||
|
}<br/>
|
||||||
olm version: {olmVersionString}<br/>
|
olm version: {olmVersionString}<br/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -953,5 +1028,5 @@ module.exports = React.createClass({
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -16,13 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require('react-dom');
|
import ReactDOM from 'react-dom';
|
||||||
var sdk = require('../../../index');
|
import url from 'url';
|
||||||
var Login = require("../../../Login");
|
import sdk from '../../../index';
|
||||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
import Login from '../../../Login';
|
||||||
var CasLogin = require("../../views/login/CasLogin");
|
|
||||||
var ServerConfig = require("../../views/login/ServerConfig");
|
// For validating phone numbers without country codes
|
||||||
|
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wire component which glues together login UI components and Login logic
|
* A wire component which glues together login UI components and Login logic
|
||||||
|
@ -52,20 +54,21 @@ 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: "",
|
||||||
|
currentFlow: "m.login.password",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -73,20 +76,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,23 +123,36 @@ module.exports = React.createClass({
|
||||||
this.setState({ username: username });
|
this.setState({ username: username });
|
||||||
},
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onPhoneCountryChanged: function(phoneCountry) {
|
||||||
var self = this;
|
this.setState({ phoneCountry: phoneCountry });
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(phoneNumber) {
|
||||||
|
// Validate the phone number entered
|
||||||
|
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
|
||||||
|
this.setState({ errorText: 'The phone number entered looks invalid' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
enteredHomeserverUrl: newHsUrl,
|
phoneNumber: phoneNumber,
|
||||||
errorText: null, // reset err messages
|
errorText: null,
|
||||||
}, function() {
|
|
||||||
self._initLoginLogic(newHsUrl);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
let newState = {
|
||||||
enteredIdentityServerUrl: newIsUrl,
|
|
||||||
errorText: null, // reset err messages
|
errorText: null, // reset err messages
|
||||||
}, function() {
|
};
|
||||||
self._initLoginLogic(null, newIsUrl);
|
if (config.hsUrl !== undefined) {
|
||||||
|
newState.enteredHomeserverUrl = config.hsUrl;
|
||||||
|
}
|
||||||
|
if (config.isUrl !== undefined) {
|
||||||
|
newState.enteredIdentityServerUrl = config.isUrl;
|
||||||
|
}
|
||||||
|
this.setState(newState, function() {
|
||||||
|
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -151,25 +168,28 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
this._loginLogic = loginLogic;
|
this._loginLogic = loginLogic;
|
||||||
|
|
||||||
loginLogic.getFlows().then(function(flows) {
|
|
||||||
// old behaviour was to always use the first flow without presenting
|
|
||||||
// options. This works in most cases (we don't have a UI for multiple
|
|
||||||
// logins so let's skip that for now).
|
|
||||||
loginLogic.chooseFlow(0);
|
|
||||||
}, function(err) {
|
|
||||||
self._setStateFromError(err, false);
|
|
||||||
}).finally(function() {
|
|
||||||
self.setState({
|
|
||||||
busy: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
enteredHomeserverUrl: hsUrl,
|
enteredHomeserverUrl: hsUrl,
|
||||||
enteredIdentityServerUrl: isUrl,
|
enteredIdentityServerUrl: isUrl,
|
||||||
busy: true,
|
busy: true,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loginLogic.getFlows().then(function(flows) {
|
||||||
|
// old behaviour was to always use the first flow without presenting
|
||||||
|
// options. This works in most cases (we don't have a UI for multiple
|
||||||
|
// logins so let's skip that for now).
|
||||||
|
loginLogic.chooseFlow(0);
|
||||||
|
self.setState({
|
||||||
|
currentFlow: self._getCurrentFlowStep(),
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
self._setStateFromError(err, false);
|
||||||
|
}).finally(function() {
|
||||||
|
self.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getCurrentFlowStep: function() {
|
_getCurrentFlowStep: function() {
|
||||||
|
@ -221,16 +241,29 @@ module.exports = React.createClass({
|
||||||
componentForStep: function(step) {
|
componentForStep: function(step) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 'm.login.password':
|
case 'm.login.password':
|
||||||
|
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||||
|
// HSs that are not matrix.org may not be configured to have their
|
||||||
|
// domain name === domain part.
|
||||||
|
let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname;
|
||||||
|
if (hsDomain !== 'matrix.org') {
|
||||||
|
hsDomain = null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
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}
|
||||||
|
hsDomain={hsDomain}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'm.login.cas':
|
case 'm.login.cas':
|
||||||
|
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||||
return (
|
return (
|
||||||
<CasLogin onSubmit={this.onCasLogin} />
|
<CasLogin onSubmit={this.onCasLogin} />
|
||||||
);
|
);
|
||||||
|
@ -248,10 +281,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
|
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||||
|
|
||||||
var loginAsGuestJsx;
|
var loginAsGuestJsx;
|
||||||
if (this.props.enableGuest) {
|
if (this.props.enableGuest) {
|
||||||
|
@ -277,15 +311,14 @@ module.exports = React.createClass({
|
||||||
<h2>Sign in
|
<h2>Sign in
|
||||||
{ loader }
|
{ loader }
|
||||||
</h2>
|
</h2>
|
||||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
{ this.componentForStep(this.state.currentFlow) }
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
|
||||||
delayTimeMs={1000}/>
|
delayTimeMs={1000}/>
|
||||||
<div className="mx_Login_error">
|
<div className="mx_Login_error">
|
||||||
{ this.state.errorText }
|
{ this.state.errorText }
|
||||||
|
|
|
@ -123,18 +123,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
this.setState({
|
let newState = {};
|
||||||
hsUrl: newHsUrl,
|
if (config.hsUrl !== undefined) {
|
||||||
|
newState.hsUrl = config.hsUrl;
|
||||||
|
}
|
||||||
|
if (config.isUrl !== undefined) {
|
||||||
|
newState.isUrl = config.isUrl;
|
||||||
|
}
|
||||||
|
this.setState(newState, function() {
|
||||||
|
this._replaceClient();
|
||||||
});
|
});
|
||||||
this._replaceClient();
|
|
||||||
},
|
|
||||||
|
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
|
||||||
this.setState({
|
|
||||||
isUrl: newIsUrl,
|
|
||||||
});
|
|
||||||
this._replaceClient();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_replaceClient: function() {
|
_replaceClient: function() {
|
||||||
|
@ -155,10 +154,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onUIAuthFinished: function(success, response, extra) {
|
_onUIAuthFinished: function(success, response, extra) {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
let msg = response.message || response.toString();
|
||||||
|
// can we give a better error message?
|
||||||
|
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
|
let msisdn_available = false;
|
||||||
|
for (const flow of response.available_flows) {
|
||||||
|
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||||
|
}
|
||||||
|
if (!msisdn_available) {
|
||||||
|
msg = "This server does not support authentication with a phone number";
|
||||||
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
doingUIAuth: false,
|
doingUIAuth: false,
|
||||||
errorText: response.message || response.toString(),
|
errorText: msg,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -185,7 +195,6 @@ module.exports = React.createClass({
|
||||||
const teamToken = data.team_token;
|
const teamToken = data.team_token;
|
||||||
// Store for use /w welcome pages
|
// Store for use /w welcome pages
|
||||||
window.localStorage.setItem('mx_team_token', teamToken);
|
window.localStorage.setItem('mx_team_token', teamToken);
|
||||||
this.props.onTeamMemberRegistered(teamToken);
|
|
||||||
|
|
||||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -262,6 +271,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;
|
||||||
|
@ -296,15 +308,20 @@ module.exports = React.createClass({
|
||||||
guestAccessToken = null;
|
guestAccessToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only send the bind params if we're sending username / pw params
|
||||||
|
// (Since we need to send no params at all to use the ones saved in the
|
||||||
|
// session).
|
||||||
|
const bindThreepids = this.state.formVals.password ? {
|
||||||
|
email: true,
|
||||||
|
msisdn: true,
|
||||||
|
} : {};
|
||||||
|
|
||||||
return this._matrixClient.register(
|
return this._matrixClient.register(
|
||||||
this.state.formVals.username,
|
this.state.formVals.username,
|
||||||
this.state.formVals.password,
|
this.state.formVals.password,
|
||||||
undefined, // session id: included in the auth dict already
|
undefined, // session id: included in the auth dict already
|
||||||
auth,
|
auth,
|
||||||
// Only send the bind_email param if we're sending username / pw params
|
bindThreepids,
|
||||||
// (Since we need to send no params at all to use the ones saved in the
|
|
||||||
// session).
|
|
||||||
Boolean(this.state.formVals.username) || undefined,
|
|
||||||
guestAccessToken,
|
guestAccessToken,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -355,6 +372,8 @@ module.exports = React.createClass({
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
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={guestUsername}
|
guestUsername={guestUsername}
|
||||||
|
@ -370,8 +389,7 @@ module.exports = React.createClass({
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
|
||||||
delayTimeMs={1000}
|
delayTimeMs={1000}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,7 +59,9 @@ module.exports = React.createClass({
|
||||||
ContentRepo.getHttpUriForMxc(
|
ContentRepo.getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
props.oobData.avatarUrl,
|
props.oobData.avatarUrl,
|
||||||
props.width, props.height, props.resizeMethod
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
|
props.resizeMethod
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
this.getRoomAvatarUrl(props),
|
||||||
this.getOneToOneAvatar(props),
|
this.getOneToOneAvatar(props),
|
||||||
|
@ -74,7 +76,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return props.room.getAvatarUrl(
|
return props.room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
props.width, props.height, props.resizeMethod,
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
|
props.resizeMethod,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -103,14 +107,18 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return theOtherGuy.getAvatarUrl(
|
return theOtherGuy.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
props.width, props.height, props.resizeMethod,
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
|
props.resizeMethod,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else if (userIds.length == 1) {
|
} else if (userIds.length == 1) {
|
||||||
return mlist[userIds[0]].getAvatarUrl(
|
return mlist[userIds[0]].getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
props.width, props.height, props.resizeMethod,
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
false
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
|
props.resizeMethod,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -46,7 +47,19 @@ export default React.createClass({
|
||||||
children: React.PropTypes.node,
|
children: React.PropTypes.node,
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(e) {
|
componentWillMount: function() {
|
||||||
|
this.priorActiveElement = document.activeElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this.priorActiveElement !== null) {
|
||||||
|
this.priorActiveElement.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Must be when the key is released (and not pressed) otherwise componentWillUnmount
|
||||||
|
// will focus another element which will receive future key events
|
||||||
|
_onKeyUp: function(e) {
|
||||||
if (e.keyCode === KeyCode.ESCAPE) {
|
if (e.keyCode === KeyCode.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -65,15 +78,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 onKeyUp={this._onKeyUp} 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,
|
||||||
|
};
|
|
@ -30,15 +30,6 @@ 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: {
|
||||||
|
@ -111,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);
|
||||||
}
|
}
|
||||||
|
@ -211,20 +211,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the query isn't a user we know about, but is a
|
// If the query is a valid address, add an entry for that
|
||||||
// valid address, add an entry for that
|
// This is important, otherwise there's no way to invite
|
||||||
if (queryList.length == 0) {
|
// 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();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,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) {
|
||||||
|
@ -311,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: "Failed to invite",
|
||||||
description: err.toString()
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
@ -324,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: "Failed to invite user",
|
||||||
description: err.toString()
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
@ -345,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: "Failed to invite",
|
||||||
description: err.toString()
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
@ -381,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 {
|
||||||
|
|
|
@ -50,6 +50,12 @@ export default React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
if (this.props.focus) {
|
||||||
|
this.refs.button.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
|
@ -59,7 +65,7 @@ export default React.createClass({
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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,6 +32,7 @@ export default React.createClass({
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
extraButtons: null,
|
||||||
button: "OK",
|
button: "OK",
|
||||||
focus: true,
|
focus: true,
|
||||||
hasCancelButton: true,
|
hasCancelButton: true,
|
||||||
|
@ -48,6 +47,12 @@ export default React.createClass({
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
if (this.props.focus) {
|
||||||
|
this.refs.button.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const cancelButton = this.props.hasCancelButton ? (
|
const cancelButton = this.props.hasCancelButton ? (
|
||||||
|
@ -64,9 +69,10 @@ export default React.createClass({
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
|
{this.props.extraButtons}
|
||||||
{cancelButton}
|
{cancelButton}
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -149,7 +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 devices that you haven't seen before.
|
"{this.props.room.name}" contains devices that you haven't seen before.
|
||||||
</h4>
|
</h4>
|
||||||
{ warning }
|
{ warning }
|
||||||
Unknown devices:
|
Unknown devices:
|
||||||
|
|
|
@ -27,11 +27,13 @@ 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";
|
||||||
|
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||||
|
"mx_AccessibleButton";
|
||||||
return React.createElement(element, restProps, children);
|
return React.createElement(element, restProps, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
src/components/views/elements/ActionButton.js
Normal file
80
src/components/views/elements/ActionButton.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
import AccessibleButton from './AccessibleButton';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'RoleButton',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
action: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
iconPath: PropTypes.string.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
size: "25",
|
||||||
|
tooltip: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
showTooltip: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_onClick: function(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
dis.dispatch({action: this.props.action});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onMouseEnter: function() {
|
||||||
|
if (this.props.tooltip) this.setState({showTooltip: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onMouseLeave: function() {
|
||||||
|
this.setState({showTooltip: false});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
let tooltip;
|
||||||
|
if (this.state.showTooltip) {
|
||||||
|
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||||
|
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleButton className="mx_RoleButton"
|
||||||
|
onClick={this._onClick}
|
||||||
|
onMouseEnter={this._onMouseEnter}
|
||||||
|
onMouseLeave={this._onMouseLeave}
|
||||||
|
>
|
||||||
|
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||||
|
{tooltip}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -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.
|
||||||
|
@ -138,7 +139,7 @@ export default React.createClass({
|
||||||
onClick={this.onClick.bind(this, i)}
|
onClick={this.onClick.bind(this, i)}
|
||||||
onMouseEnter={this.onMouseEnter.bind(this, i)}
|
onMouseEnter={this.onMouseEnter.bind(this, i)}
|
||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
key={this.props.addressList[i].userId}
|
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
||||||
ref={(ref) => { this.addressListElement = ref; }}
|
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" />
|
||||||
|
|
38
src/components/views/elements/CreateRoomButton.js
Normal file
38
src/components/views/elements/CreateRoomButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const CreateRoomButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_create_chat"
|
||||||
|
label="Create new room"
|
||||||
|
iconPath="img/icons-create-room.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CreateRoomButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateRoomButton;
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
329
src/components/views/elements/Dropdown.js
Normal file
329
src/components/views/elements/Dropdown.js
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
/*
|
||||||
|
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) {
|
||||||
|
if (!nextProps.children || nextProps.children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._reindexChildren(nextProps.children);
|
||||||
|
const firstChild = 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 (options.length === 0) {
|
||||||
|
return [<div className="mx_Dropdown_option">
|
||||||
|
No results
|
||||||
|
</div>];
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let currentValue;
|
||||||
|
|
||||||
|
const menuStyle = {};
|
||||||
|
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||||
|
|
||||||
|
let menu;
|
||||||
|
if (this.state.expanded) {
|
||||||
|
if (this.props.searchEnabled) {
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentValue) {
|
||||||
|
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,
|
||||||
|
searchEnabled: React.PropTypes.bool,
|
||||||
|
// 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,
|
||||||
|
}
|
38
src/components/views/elements/HomeButton.js
Normal file
38
src/components/views/elements/HomeButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const HomeButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_home_page"
|
||||||
|
label="Welcome page"
|
||||||
|
iconPath="img/icons-home.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HomeButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeButton;
|
|
@ -221,6 +221,8 @@ module.exports = React.createClass({
|
||||||
"banned": beConjugated + " banned",
|
"banned": beConjugated + " banned",
|
||||||
"unbanned": beConjugated + " unbanned",
|
"unbanned": beConjugated + " unbanned",
|
||||||
"kicked": beConjugated + " kicked",
|
"kicked": beConjugated + " kicked",
|
||||||
|
"changed_name": "changed name",
|
||||||
|
"changed_avatar": "changed avatar",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Object.keys(map).includes(t)) {
|
if (Object.keys(map).includes(t)) {
|
||||||
|
@ -289,7 +291,24 @@ module.exports = React.createClass({
|
||||||
switch (e.mxEvent.getContent().membership) {
|
switch (e.mxEvent.getContent().membership) {
|
||||||
case 'invite': return 'invited';
|
case 'invite': return 'invited';
|
||||||
case 'ban': return 'banned';
|
case 'ban': return 'banned';
|
||||||
case 'join': return 'joined';
|
case 'join':
|
||||||
|
if (e.mxEvent.getPrevContent().membership === 'join') {
|
||||||
|
if (e.mxEvent.getContent().displayname !==
|
||||||
|
e.mxEvent.getPrevContent().displayname)
|
||||||
|
{
|
||||||
|
return 'changed_name';
|
||||||
|
}
|
||||||
|
else if (e.mxEvent.getContent().avatar_url !==
|
||||||
|
e.mxEvent.getPrevContent().avatar_url)
|
||||||
|
{
|
||||||
|
return 'changed_avatar';
|
||||||
|
}
|
||||||
|
// console.log("MELS ignoring duplicate membership join event");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'joined';
|
||||||
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||||
switch (e.mxEvent.getPrevContent().membership) {
|
switch (e.mxEvent.getPrevContent().membership) {
|
||||||
|
@ -350,6 +369,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const eventsToRender = this.props.events;
|
const eventsToRender = this.props.events;
|
||||||
|
const eventIds = eventsToRender.map(e => e.getId()).join(',');
|
||||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||||
const expanded = this.state.expanded || fewEvents;
|
const expanded = this.state.expanded || fewEvents;
|
||||||
|
|
||||||
|
@ -360,7 +380,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (fewEvents) {
|
if (fewEvents) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberEventListSummary">
|
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||||
{expandedEvents}
|
{expandedEvents}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -418,7 +438,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberEventListSummary">
|
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||||
{toggleButton}
|
{toggleButton}
|
||||||
{summaryContainer}
|
{summaryContainer}
|
||||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
src/components/views/elements/RoomDirectoryButton.js
Normal file
38
src/components/views/elements/RoomDirectoryButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const RoomDirectoryButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_room_directory"
|
||||||
|
label="Room directory"
|
||||||
|
iconPath="img/icons-directory.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomDirectoryButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomDirectoryButton;
|
38
src/components/views/elements/SettingsButton.js
Normal file
38
src/components/views/elements/SettingsButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const SettingsButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_user_settings"
|
||||||
|
label="Settings"
|
||||||
|
iconPath="img/icons-settings.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsButton;
|
38
src/components/views/elements/StartChatButton.js
Normal file
38
src/components/views/elements/StartChatButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const StartChatButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_create_chat"
|
||||||
|
label="Start chat"
|
||||||
|
iconPath="img/icons-people.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StartChatButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartChatButton;
|
127
src/components/views/login/CountryDropdown.js
Normal file
127
src/components/views/login/CountryDropdown.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CountryDropdown extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onSearchChange = this._onSearchChange.bind(this);
|
||||||
|
this._onOptionChange = this._onOptionChange.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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchChange(search) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onOptionChange(iso2) {
|
||||||
|
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_flagImgForIso2(iso2) {
|
||||||
|
// Unicode Regional Indicator Symbol letter 'A'
|
||||||
|
const RIS_A = 0x1F1E6;
|
||||||
|
const ASCII_A = 65;
|
||||||
|
return charactersToImageNode(iso2, true,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined;
|
||||||
|
|
||||||
|
return <Dropdown className={this.props.className}
|
||||||
|
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||||
|
menuWidth={298} getShortOption={getShortOption}
|
||||||
|
value={value} searchEnabled={true}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CountryDropdown.propTypes = {
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
isSmall: React.PropTypes.bool,
|
||||||
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
};
|
|
@ -16,6 +16,8 @@ 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';
|
||||||
|
|
||||||
|
@ -158,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) {
|
||||||
|
@ -168,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 (
|
||||||
|
@ -255,6 +263,137 @@ export const EmailIdentityAuthEntry = React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
|
||||||
|
@ -313,6 +452,7 @@ const AuthEntryComponents = [
|
||||||
PasswordAuthEntry,
|
PasswordAuthEntry,
|
||||||
RecaptchaAuthEntry,
|
RecaptchaAuthEntry,
|
||||||
EmailIdentityAuthEntry,
|
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,66 +18,164 @@ 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';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pure UI component which displays a username/password form.
|
* A pure UI component which displays a username/password form.
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
class PasswordLogin extends React.Component {
|
||||||
propTypes: {
|
static defaultProps = {
|
||||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
onUsernameChanged: function() {},
|
||||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
onPasswordChanged: function() {},
|
||||||
initialUsername: React.PropTypes.string,
|
onPhoneCountryChanged: function() {},
|
||||||
initialPassword: React.PropTypes.string,
|
onPhoneNumberChanged: function() {},
|
||||||
onUsernameChanged: React.PropTypes.func,
|
initialUsername: "",
|
||||||
onPasswordChanged: React.PropTypes.func,
|
initialPhoneCountry: "",
|
||||||
loginIncorrect: React.PropTypes.bool,
|
initialPhoneNumber: "",
|
||||||
},
|
initialPassword: "",
|
||||||
|
loginIncorrect: false,
|
||||||
|
hsDomain: "",
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultProps: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
onUsernameChanged: function() {},
|
this.state = {
|
||||||
onPasswordChanged: function() {},
|
|
||||||
initialUsername: "",
|
|
||||||
initialPassword: "",
|
|
||||||
loginIncorrect: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
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,
|
||||||
|
loginType: PasswordLogin.LOGIN_FIELD_MXID,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||||
|
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||||
|
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
||||||
|
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||||
|
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||||
|
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
this._passwordField = null;
|
this._passwordField = null;
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||||
field_input_incorrect(this._passwordField);
|
field_input_incorrect(this._passwordField);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onSubmitForm: function(ev) {
|
onSubmitForm(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onSubmit(this.state.username, this.state.password);
|
this.props.onSubmit(
|
||||||
},
|
this.state.username,
|
||||||
|
this.state.phoneCountry,
|
||||||
|
this.state.phoneNumber,
|
||||||
|
this.state.password,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onUsernameChanged: function(ev) {
|
onUsernameChanged(ev) {
|
||||||
this.setState({username: ev.target.value});
|
this.setState({username: ev.target.value});
|
||||||
this.props.onUsernameChanged(ev.target.value);
|
this.props.onUsernameChanged(ev.target.value);
|
||||||
},
|
}
|
||||||
|
|
||||||
onPasswordChanged: function(ev) {
|
onLoginTypeChange(loginType) {
|
||||||
|
this.setState({
|
||||||
|
loginType: loginType,
|
||||||
|
username: "" // Reset because email and username use the same state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPhoneCountryChanged(country) {
|
||||||
|
this.setState({
|
||||||
|
phoneCountry: country.iso2,
|
||||||
|
phonePrefix: country.prefix,
|
||||||
|
});
|
||||||
|
this.props.onPhoneCountryChanged(country.iso2);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPhoneNumberChanged(ev) {
|
||||||
|
this.setState({phoneNumber: ev.target.value});
|
||||||
|
this.props.onPhoneNumberChanged(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordChanged(ev) {
|
||||||
this.setState({password: ev.target.value});
|
this.setState({password: ev.target.value});
|
||||||
this.props.onPasswordChanged(ev.target.value);
|
this.props.onPasswordChanged(ev.target.value);
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
renderLoginField(loginType) {
|
||||||
|
switch(loginType) {
|
||||||
|
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||||
|
return <input
|
||||||
|
className="mx_Login_field mx_Login_email"
|
||||||
|
key="email_input"
|
||||||
|
type="text"
|
||||||
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
onChange={this.onUsernameChanged}
|
||||||
|
placeholder="joe@example.com"
|
||||||
|
value={this.state.username}
|
||||||
|
autoFocus
|
||||||
|
/>;
|
||||||
|
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||||
|
const mxidInputClasses = classNames({
|
||||||
|
"mx_Login_field": true,
|
||||||
|
"mx_Login_username": true,
|
||||||
|
"mx_Login_field_has_prefix": true,
|
||||||
|
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain),
|
||||||
|
});
|
||||||
|
let suffix = null;
|
||||||
|
if (this.props.hsDomain) {
|
||||||
|
suffix = <div className="mx_Login_field_suffix">
|
||||||
|
:{this.props.hsDomain}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return <div className="mx_Login_field_group">
|
||||||
|
<div className="mx_Login_field_prefix">@</div>
|
||||||
|
<input
|
||||||
|
className={mxidInputClasses}
|
||||||
|
key="username_input"
|
||||||
|
type="text"
|
||||||
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
onChange={this.onUsernameChanged}
|
||||||
|
placeholder="username"
|
||||||
|
value={this.state.username}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{suffix}
|
||||||
|
</div>;
|
||||||
|
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
|
const prefix = this.state.phonePrefix;
|
||||||
|
return <div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
ref="phone_country"
|
||||||
|
onOptionChange={this.onPhoneCountryChanged}
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<div className="mx_Login_field_group">
|
||||||
|
<div className="mx_Login_field_prefix">+{prefix}</div>
|
||||||
|
<input
|
||||||
|
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
|
||||||
|
ref="phoneNumber"
|
||||||
|
key="phone_input"
|
||||||
|
type="text"
|
||||||
|
name="phoneNumber"
|
||||||
|
onChange={this.onPhoneNumberChanged}
|
||||||
|
placeholder="Mobile phone number"
|
||||||
|
value={this.state.phoneNumber}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
var forgotPasswordJsx;
|
var forgotPasswordJsx;
|
||||||
|
|
||||||
if (this.props.onForgotPasswordClick) {
|
if (this.props.onForgotPasswordClick) {
|
||||||
|
@ -92,14 +191,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
error: this.props.loginIncorrect,
|
error: this.props.loginIncorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
|
const loginField = this.renderLoginField(this.state.loginType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
<input className="mx_Login_field" type="text"
|
<div className="mx_Login_type_container">
|
||||||
name="username" // make it a little easier for browser's remember-password
|
<label className="mx_Login_type_label">I want to sign in with my</label>
|
||||||
value={this.state.username} onChange={this.onUsernameChanged}
|
<Dropdown
|
||||||
placeholder="Email or user name" autoFocus />
|
className="mx_Login_type_dropdown"
|
||||||
<br />
|
value={this.state.loginType}
|
||||||
|
onOptionChange={this.onLoginTypeChange}>
|
||||||
|
<span key={PasswordLogin.LOGIN_FIELD_MXID}>Matrix ID</span>
|
||||||
|
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>Email Address</span>
|
||||||
|
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>Phone</span>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
{loginField}
|
||||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||||
name="password"
|
name="password"
|
||||||
value={this.state.password} onChange={this.onPasswordChanged}
|
value={this.state.password} onChange={this.onPasswordChanged}
|
||||||
|
@ -111,4 +221,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
|
||||||
|
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
|
||||||
|
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||||
|
|
||||||
|
PasswordLogin.propTypes = {
|
||||||
|
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||||
|
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||||
|
initialUsername: React.PropTypes.string,
|
||||||
|
initialPhoneCountry: React.PropTypes.string,
|
||||||
|
initialPhoneNumber: React.PropTypes.string,
|
||||||
|
initialPassword: React.PropTypes.string,
|
||||||
|
onUsernameChanged: React.PropTypes.func,
|
||||||
|
onPhoneCountryChanged: React.PropTypes.func,
|
||||||
|
onPhoneNumberChanged: React.PropTypes.func,
|
||||||
|
onPasswordChanged: React.PropTypes.func,
|
||||||
|
loginIncorrect: React.PropTypes.bool,
|
||||||
|
hsDomain: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PasswordLogin;
|
||||||
|
|
|
@ -19,9 +19,12 @@ import React from 'react';
|
||||||
import { field_input_incorrect } from '../../../UiEffects';
|
import { field_input_incorrect } from '../../../UiEffects';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Email from '../../../email';
|
import Email from '../../../email';
|
||||||
|
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
|
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||||
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
const FIELD_USERNAME = 'field_username';
|
const FIELD_USERNAME = 'field_username';
|
||||||
const FIELD_PASSWORD = 'field_password';
|
const FIELD_PASSWORD = 'field_password';
|
||||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||||
|
@ -35,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({
|
||||||
|
@ -71,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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -85,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;
|
||||||
|
@ -118,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,6 +184,11 @@ module.exports = React.createClass({
|
||||||
const emailValid = email === '' || Email.looksValid(email);
|
const emailValid = email === '' || Email.looksValid(email);
|
||||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
const phoneNumber = this.refs.phoneNumber.value;
|
||||||
|
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||||
|
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||||
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
||||||
|
@ -233,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:
|
||||||
|
@ -251,6 +268,13 @@ module.exports = React.createClass({
|
||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onPhoneCountryChange(newVal) {
|
||||||
|
this.setState({
|
||||||
|
phoneCountry: newVal.iso2,
|
||||||
|
phonePrefix: newVal.prefix,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -286,6 +310,31 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<div className="mx_Login_field_group">
|
||||||
|
<div className="mx_Login_field_prefix">+{this.state.phonePrefix}</div>
|
||||||
|
<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',
|
||||||
|
'mx_Login_field_has_prefix'
|
||||||
|
)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||||
|
value={self.state.phoneNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const registerButton = (
|
const registerButton = (
|
||||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||||
);
|
);
|
||||||
|
@ -300,6 +349,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')}
|
||||||
|
|
|
@ -27,8 +27,7 @@ module.exports = React.createClass({
|
||||||
displayName: 'ServerConfig',
|
displayName: 'ServerConfig',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onHsUrlChanged: React.PropTypes.func,
|
onServerConfigChange: React.PropTypes.func,
|
||||||
onIsUrlChanged: React.PropTypes.func,
|
|
||||||
|
|
||||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||||
// they are used if the user has not overridden them with a custom URL.
|
// they are used if the user has not overridden them with a custom URL.
|
||||||
|
@ -50,8 +49,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
onHsUrlChanged: function() {},
|
onServerConfigChange: function() {},
|
||||||
onIsUrlChanged: function() {},
|
|
||||||
customHsUrl: "",
|
customHsUrl: "",
|
||||||
customIsUrl: "",
|
customIsUrl: "",
|
||||||
withToggleButton: false,
|
withToggleButton: false,
|
||||||
|
@ -75,7 +73,10 @@ module.exports = React.createClass({
|
||||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||||
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||||
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
||||||
this.props.onHsUrlChanged(hsUrl);
|
this.props.onServerConfigChange({
|
||||||
|
hsUrl : this.state.hs_url,
|
||||||
|
isUrl : this.state.is_url,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -85,7 +86,10 @@ module.exports = React.createClass({
|
||||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||||
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||||
if (isUrl === "") isUrl = this.props.defaultIsUrl;
|
if (isUrl === "") isUrl = this.props.defaultIsUrl;
|
||||||
this.props.onIsUrlChanged(isUrl);
|
this.props.onServerConfigChange({
|
||||||
|
hsUrl : this.state.hs_url,
|
||||||
|
isUrl : this.state.is_url,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -102,12 +106,16 @@ module.exports = React.createClass({
|
||||||
configVisible: visible
|
configVisible: visible
|
||||||
});
|
});
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
this.props.onHsUrlChanged(this.props.defaultHsUrl);
|
this.props.onServerConfigChange({
|
||||||
this.props.onIsUrlChanged(this.props.defaultIsUrl);
|
hsUrl : this.props.defaultHsUrl,
|
||||||
|
isUrl : this.props.defaultIsUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.props.onHsUrlChanged(this.state.hs_url);
|
this.props.onServerConfigChange({
|
||||||
this.props.onIsUrlChanged(this.state.is_url);
|
hsUrl : this.state.hs_url,
|
||||||
|
isUrl : this.state.is_url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -346,7 +346,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank">
|
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
|
||||||
{ fileName }
|
{ fileName }
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageBody_size">
|
<div className="mx_MImageBody_size">
|
||||||
|
@ -360,7 +360,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={contentUrl} target="_blank" rel="noopener">
|
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
|
||||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
||||||
Download {text}
|
Download {text}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -56,6 +56,7 @@ module.exports = React.createClass({
|
||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
const ImageView = sdk.getComponent("elements.ImageView");
|
||||||
const params = {
|
const params = {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
|
name: content.body && content.body.length > 0 ? content.body : 'Attachment',
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,16 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getReadAvatars: function() {
|
getReadAvatars: function() {
|
||||||
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
|
||||||
var avatars = [];
|
// return early if there are no read receipts
|
||||||
var left = 0;
|
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||||
|
return (<span className="mx_EventTile_readAvatars"></span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||||
|
const avatars = [];
|
||||||
|
const receiptOffset = 15;
|
||||||
|
let left = 0;
|
||||||
|
|
||||||
// It's possible that the receipt was sent several days AFTER the event.
|
// 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 +313,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;
|
||||||
|
@ -318,11 +330,6 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
this.props.readReceiptMap[userId] = readReceiptInfo;
|
this.props.readReceiptMap[userId] = readReceiptInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
||||||
|
@ -343,7 +350,7 @@ 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={{ right: -(left - 15) }}>{ remainder }+
|
style={{ right: -(left - receiptOffset) }}>{ remainder }+
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,7 +363,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)', ''),
|
||||||
});
|
});
|
||||||
|
@ -372,6 +379,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');
|
||||||
|
@ -383,8 +401,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
var msgtype = content.msgtype;
|
var msgtype = content.msgtype;
|
||||||
var eventType = this.props.mxEvent.getType();
|
var eventType = this.props.mxEvent.getType();
|
||||||
|
|
||||||
// Info messages are basically information about commands processed on a
|
// Info messages are basically information about commands processed on a room
|
||||||
// room, or emote messages
|
|
||||||
var isInfoMessage = (eventType !== 'm.room.message');
|
var isInfoMessage = (eventType !== 'm.room.message');
|
||||||
|
|
||||||
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
|
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
|
||||||
|
@ -396,6 +413,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,
|
||||||
|
@ -411,9 +429,14 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
menu: this.state.menu,
|
menu: this.state.menu,
|
||||||
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: msgtype === 'm.bad.encrypted',
|
||||||
|
mx_EventTile_emote: msgtype === 'm.emote',
|
||||||
|
mx_EventTile_redacted: isRedacted,
|
||||||
});
|
});
|
||||||
var permalink = "https://matrix.to/#/" + 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();
|
||||||
|
|
||||||
|
@ -486,6 +509,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());
|
||||||
|
@ -493,15 +518,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" >
|
||||||
|
@ -527,10 +552,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>
|
||||||
|
@ -545,8 +574,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"
|
||||||
|
@ -564,7 +593,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: "Failed to kick",
|
||||||
description: err.message
|
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
).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;
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||||
this.onEvent = this.onEvent.bind(this);
|
this.onEvent = this.onEvent.bind(this);
|
||||||
|
this.onPageUnload = this.onPageUnload.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
autocompleteQuery: '',
|
autocompleteQuery: '',
|
||||||
|
@ -50,7 +51,7 @@ export default class MessageComposer extends React.Component {
|
||||||
inputState: {
|
inputState: {
|
||||||
style: [],
|
style: [],
|
||||||
blockType: null,
|
blockType: null,
|
||||||
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
|
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
|
||||||
wordCount: 0,
|
wordCount: 0,
|
||||||
},
|
},
|
||||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||||
|
@ -64,12 +65,21 @@ export default class MessageComposer extends React.Component {
|
||||||
// marked as encrypted.
|
// marked as encrypted.
|
||||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||||
MatrixClientPeg.get().on("event", this.onEvent);
|
MatrixClientPeg.get().on("event", this.onEvent);
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.onPageUnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||||
}
|
}
|
||||||
|
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageUnload(event) {
|
||||||
|
if (this.messageComposerInput) {
|
||||||
|
this.messageComposerInput.sentHistory.saveLastTextEntry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
|
@ -91,8 +101,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 +111,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 +182,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpArrow() {
|
onUpArrow() {
|
||||||
return this.refs.autocomplete.onUpArrow();
|
return this.refs.autocomplete.onUpArrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDownArrow() {
|
onDownArrow() {
|
||||||
|
@ -299,6 +310,7 @@ export default class MessageComposer extends React.Component {
|
||||||
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} />,
|
||||||
|
|
|
@ -96,8 +96,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.onAction = this.onAction.bind(this);
|
||||||
|
this.handleReturn = this.handleReturn.bind(this);
|
||||||
|
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||||
|
this.handlePastedFiles = this.handlePastedFiles.bind(this);
|
||||||
|
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||||
|
this.setEditorState = this.setEditorState.bind(this);
|
||||||
|
this.onUpArrow = this.onUpArrow.bind(this);
|
||||||
|
this.onDownArrow = this.onDownArrow.bind(this);
|
||||||
|
this.onTab = this.onTab.bind(this);
|
||||||
|
this.onEscape = this.onEscape.bind(this);
|
||||||
|
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||||
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// whether we're in rich text or markdown mode
|
// whether we're in rich text or markdown mode
|
||||||
|
@ -261,6 +273,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTyping(isTyping) {
|
sendTyping(isTyping) {
|
||||||
|
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||||
|
@ -404,10 +417,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
handleReturn = (ev) => {
|
handlePastedFiles(files) {
|
||||||
if(ev.shiftKey) {
|
this.props.onUploadFileSelected(files, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReturn(ev) {
|
||||||
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -442,7 +459,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: err.message,
|
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (cmd.error) {
|
} else if (cmd.error) {
|
||||||
|
@ -473,9 +490,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;
|
||||||
}
|
}
|
||||||
|
@ -686,6 +703,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
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}
|
||||||
|
@ -697,3 +715,28 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageComposerInput.propTypes = {
|
||||||
|
tabComplete: React.PropTypes.any,
|
||||||
|
|
||||||
|
// a callback which is called when the height of the composer is
|
||||||
|
// changed due to a change in content.
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
|
|
||||||
|
// js-sdk Room object
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// called with current plaintext content (as a string) whenever it changes
|
||||||
|
onContentChanged: React.PropTypes.func,
|
||||||
|
|
||||||
|
onUpArrow: React.PropTypes.func,
|
||||||
|
|
||||||
|
onDownArrow: React.PropTypes.func,
|
||||||
|
|
||||||
|
onUploadFileSelected: React.PropTypes.func,
|
||||||
|
|
||||||
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
|
tryComplete: React.PropTypes.func,
|
||||||
|
|
||||||
|
onInputStateChanged: React.PropTypes.func,
|
||||||
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
import UserSettingsStore from "../../../UserSettingsStore";
|
||||||
|
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var KeyCode = require("../../../KeyCode");
|
var KeyCode = require("../../../KeyCode");
|
||||||
|
@ -311,7 +312,7 @@ export default React.createClass({
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: err.message
|
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -420,6 +421,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
sendTyping: function(isTyping) {
|
sendTyping: function(isTyping) {
|
||||||
|
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||||
|
|
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
if (this.props.activeAgo >= 0) {
|
if (this.props.activeAgo >= 0) {
|
||||||
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago");
|
var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo));
|
||||||
// var ago = this.getDuration(this.props.activeAgo) + " ago";
|
// var ago = this.getDuration(this.props.activeAgo) + " ago";
|
||||||
// if (this.props.currentlyActive) ago += " (now?)";
|
// if (this.props.currentlyActive) ago += " (now?)";
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -21,15 +22,23 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var CallHandler = require('../../../CallHandler');
|
var CallHandler = require('../../../CallHandler');
|
||||||
var RoomListSorter = require("../../../RoomListSorter");
|
var RoomListSorter = require("../../../RoomListSorter");
|
||||||
var Unread = require('../../../Unread');
|
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
var Rooms = require('../../../Rooms');
|
var Rooms = require('../../../Rooms');
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var Receipt = require('../../../utils/Receipt');
|
var Receipt = require('../../../utils/Receipt');
|
||||||
|
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
var HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
|
const VERBS = {
|
||||||
|
'm.favourite': 'favourite',
|
||||||
|
'im.vector.fake.direct': 'tag direct chat',
|
||||||
|
'im.vector.fake.recent': 'restore',
|
||||||
|
'm.lowpriority': 'demote',
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomList',
|
displayName: 'RoomList',
|
||||||
|
@ -37,13 +46,23 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: React.PropTypes.any,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
currentRoom: React.PropTypes.string,
|
selectedRoom: React.PropTypes.string,
|
||||||
searchFilter: React.PropTypes.string,
|
searchFilter: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
if (nextProps.collapsed !== this.props.collapsed) return true;
|
||||||
|
if (nextProps.searchFilter !== this.props.searchFilter) return true;
|
||||||
|
if (nextState.lists !== this.state.lists ||
|
||||||
|
nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
|
||||||
|
nextState.incomingCall !== this.state.incomingCall) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
isLoadingLeftRooms: false,
|
isLoadingLeftRooms: false,
|
||||||
|
totalRoomCount: null,
|
||||||
lists: {},
|
lists: {},
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
};
|
};
|
||||||
|
@ -57,12 +76,21 @@ module.exports = React.createClass({
|
||||||
cli.on("Room.name", this.onRoomName);
|
cli.on("Room.name", this.onRoomName);
|
||||||
cli.on("Room.tags", this.onRoomTags);
|
cli.on("Room.tags", this.onRoomTags);
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
var s = this.getRoomLists();
|
// lookup for which lists a given roomId is currently in.
|
||||||
this.setState(s);
|
this.listsForRoomId = {};
|
||||||
|
|
||||||
|
this.refreshRoomList();
|
||||||
|
|
||||||
|
// order of the sublists
|
||||||
|
//this.listOrder = [];
|
||||||
|
|
||||||
|
// loop count to stop a stack overflow if the user keeps waggling the
|
||||||
|
// mouse for >30s in a row, or if running under mocha
|
||||||
|
this._delayedRefreshRoomListLoopCount = 0
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -71,7 +99,22 @@ module.exports = React.createClass({
|
||||||
this._updateStickyHeaders(true);
|
this._updateStickyHeaders(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
// short-circuit react when the room changes
|
||||||
|
// to avoid rerendering all the sublists everywhere
|
||||||
|
if (nextProps.selectedRoom !== this.props.selectedRoom) {
|
||||||
|
if (this.props.selectedRoom) {
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.select", this.props.selectedRoom, {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.select", nextProps.selectedRoom, { selected: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function(prevProps, prevState) {
|
||||||
// Reinitialise the stickyHeaders when the component is updated
|
// Reinitialise the stickyHeaders when the component is updated
|
||||||
this._updateStickyHeaders(true);
|
this._updateStickyHeaders(true);
|
||||||
this._repositionIncomingCallBox(undefined, false);
|
this._repositionIncomingCallBox(undefined, false);
|
||||||
|
@ -95,6 +138,26 @@ module.exports = React.createClass({
|
||||||
incomingCall: null
|
incomingCall: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case 'on_room_read':
|
||||||
|
// poke the right RoomTile to refresh, using the constantTimeDispatcher
|
||||||
|
// to avoid each and every RoomTile registering to the 'on_room_read' event
|
||||||
|
// XXX: if we like the constantTimeDispatcher we might want to dispatch
|
||||||
|
// directly from TimelinePanel rather than needlessly bouncing via here.
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", payload.room.roomId, {}
|
||||||
|
);
|
||||||
|
|
||||||
|
// also have to poke the right list(s)
|
||||||
|
var lists = this.listsForRoomId[payload.room.roomId];
|
||||||
|
if (lists) {
|
||||||
|
lists.forEach(list=>{
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomSubList.refreshHeader", list, { room: payload.room }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -108,7 +171,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
@ -117,10 +180,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
|
// XXX: this happens rarely; ideally we should only update the correct
|
||||||
|
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onDeleteRoom: function(roomId) {
|
onDeleteRoom: function(roomId) {
|
||||||
|
// XXX: this happens rarely; ideally we should only update the correct
|
||||||
|
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -143,6 +210,10 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onMouseOver: function(ev) {
|
||||||
|
this._lastMouseOverTs = Date.now();
|
||||||
|
},
|
||||||
|
|
||||||
onSubListHeaderClick: function(isHidden, scrollToPosition) {
|
onSubListHeaderClick: function(isHidden, scrollToPosition) {
|
||||||
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
|
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
|
||||||
this._updateStickyHeaders(true, scrollToPosition);
|
this._updateStickyHeaders(true, scrollToPosition);
|
||||||
|
@ -152,41 +223,98 @@ module.exports = React.createClass({
|
||||||
if (toStartOfTimeline) return;
|
if (toStartOfTimeline) return;
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||||
this._delayedRefreshRoomList();
|
|
||||||
|
// rather than regenerate our full roomlists, which is very heavy, we poke the
|
||||||
|
// correct sublists to just re-sort themselves. This isn't enormously reacty,
|
||||||
|
// but is much faster than the default react reconciler, or having to do voodoo
|
||||||
|
// with shouldComponentUpdate and a pleaseRefresh property or similar.
|
||||||
|
var lists = this.listsForRoomId[room.roomId];
|
||||||
|
if (lists) {
|
||||||
|
lists.forEach(list=>{
|
||||||
|
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to explicitly hit the roomtile which just changed
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", room.roomId, {}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(receiptEvent, room) {
|
onRoomReceipt: function(receiptEvent, room) {
|
||||||
// because if we read a notification, it will affect notification count
|
// because if we read a notification, it will affect notification count
|
||||||
// only bother updating if there's a receipt from us
|
// only bother updating if there's a receipt from us
|
||||||
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
|
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
|
||||||
this._delayedRefreshRoomList();
|
var lists = this.listsForRoomId[room.roomId];
|
||||||
|
if (lists) {
|
||||||
|
lists.forEach(list=>{
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomSubList.refreshHeader", list, { room: room }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to explicitly hit the roomtile which just changed
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", room.roomId, {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomName: function(room) {
|
onRoomName: function(room) {
|
||||||
this._delayedRefreshRoomList();
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", room.roomId, {}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTags: function(event, room) {
|
onRoomTags: function(event, room) {
|
||||||
|
// XXX: this happens rarely; ideally we should only update the correct
|
||||||
|
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomStateEvents: function(ev, state) {
|
onRoomStateMember: function(ev, state, member) {
|
||||||
this._delayedRefreshRoomList();
|
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
|
||||||
|
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
|
||||||
|
{
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", member.roomId, {}
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomMemberName: function(ev, member) {
|
onRoomMemberName: function(ev, member) {
|
||||||
this._delayedRefreshRoomList();
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", member.roomId, {}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAccountData: function(ev) {
|
onAccountData: function(ev) {
|
||||||
if (ev.getType() == 'm.direct') {
|
if (ev.getType() == 'm.direct') {
|
||||||
|
// XXX: this happens rarely; ideally we should only update the correct
|
||||||
|
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
}
|
||||||
|
else if (ev.getType() == 'm.push_rules') {
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||||
this.refreshRoomList();
|
// if the mouse has been moving over the RoomList in the last 500ms
|
||||||
|
// then delay the refresh further to avoid bouncing around under the
|
||||||
|
// cursor
|
||||||
|
if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
|
||||||
|
this.refreshRoomList();
|
||||||
|
this._delayedRefreshRoomListLoopCount = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._delayedRefreshRoomListLoopCount++;
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
refreshRoomList: function() {
|
refreshRoomList: function() {
|
||||||
|
@ -194,26 +322,36 @@ module.exports = React.createClass({
|
||||||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// TODO: rather than bluntly regenerating and re-sorting everything
|
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||||
// every time we see any kind of room change from the JS SDK
|
// any changes to it incrementally, updating the appropriate sublists
|
||||||
// we could do incremental updates on our copy of the state
|
// as needed.
|
||||||
// based on the room which has actually changed. This would stop
|
// Alternatively we'd do something magical with Immutable.js or similar.
|
||||||
// us re-rendering all the sublists every time anything changes anywhere
|
const lists = this.getRoomLists();
|
||||||
// in the state of the client.
|
let totalRooms = 0;
|
||||||
this.setState(this.getRoomLists());
|
for (const l of Object.values(lists)) {
|
||||||
this._lastRefreshRoomListTs = Date.now();
|
totalRooms += l.length;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
lists: this.getRoomLists(),
|
||||||
|
totalRoomCount: totalRooms,
|
||||||
|
});
|
||||||
|
|
||||||
|
// this._lastRefreshRoomListTs = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var s = { lists: {} };
|
const lists = {};
|
||||||
|
|
||||||
s.lists["im.vector.fake.invite"] = [];
|
lists["im.vector.fake.invite"] = [];
|
||||||
s.lists["m.favourite"] = [];
|
lists["m.favourite"] = [];
|
||||||
s.lists["im.vector.fake.recent"] = [];
|
lists["im.vector.fake.recent"] = [];
|
||||||
s.lists["im.vector.fake.direct"] = [];
|
lists["im.vector.fake.direct"] = [];
|
||||||
s.lists["m.lowpriority"] = [];
|
lists["m.lowpriority"] = [];
|
||||||
s.lists["im.vector.fake.archived"] = [];
|
lists["im.vector.fake.archived"] = [];
|
||||||
|
|
||||||
|
this.listsForRoomId = {};
|
||||||
|
var otherTagNames = {};
|
||||||
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
|
||||||
|
@ -226,8 +364,13 @@ module.exports = React.createClass({
|
||||||
// ", target = " + me.events.member.getStateKey() +
|
// ", target = " + me.events.member.getStateKey() +
|
||||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||||
|
|
||||||
|
if (!self.listsForRoomId[room.roomId]) {
|
||||||
|
self.listsForRoomId[room.roomId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (me.membership == "invite") {
|
if (me.membership == "invite") {
|
||||||
s.lists["im.vector.fake.invite"].push(room);
|
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
|
||||||
|
lists["im.vector.fake.invite"].push(room);
|
||||||
}
|
}
|
||||||
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
|
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
|
||||||
// skip past this room & don't put it in any lists
|
// skip past this room & don't put it in any lists
|
||||||
|
@ -237,81 +380,62 @@ module.exports = React.createClass({
|
||||||
{
|
{
|
||||||
// Used to split rooms via tags
|
// Used to split rooms via tags
|
||||||
var tagNames = Object.keys(room.tags);
|
var tagNames = Object.keys(room.tags);
|
||||||
|
|
||||||
if (tagNames.length) {
|
if (tagNames.length) {
|
||||||
for (var i = 0; i < tagNames.length; i++) {
|
for (var i = 0; i < tagNames.length; i++) {
|
||||||
var tagName = tagNames[i];
|
var tagName = tagNames[i];
|
||||||
s.lists[tagName] = s.lists[tagName] || [];
|
lists[tagName] = lists[tagName] || [];
|
||||||
s.lists[tagNames[i]].push(room);
|
lists[tagName].push(room);
|
||||||
|
self.listsForRoomId[room.roomId].push(tagName);
|
||||||
|
otherTagNames[tagName] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||||
s.lists["im.vector.fake.direct"].push(room);
|
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
|
||||||
|
lists["im.vector.fake.direct"].push(room);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
s.lists["im.vector.fake.recent"].push(room);
|
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
|
||||||
|
lists["im.vector.fake.recent"].push(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (me.membership === "leave") {
|
else if (me.membership === "leave") {
|
||||||
s.lists["im.vector.fake.archived"].push(room);
|
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
|
||||||
|
lists["im.vector.fake.archived"].push(room);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (s.lists["im.vector.fake.direct"].length == 0 &&
|
|
||||||
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
|
|
||||||
!MatrixClientPeg.get().isGuest())
|
|
||||||
{
|
|
||||||
// scan through the 'recents' list for any rooms which look like DM rooms
|
|
||||||
// and make them DM rooms
|
|
||||||
const oldRecents = s.lists["im.vector.fake.recent"];
|
|
||||||
s.lists["im.vector.fake.recent"] = [];
|
|
||||||
|
|
||||||
for (const room of oldRecents) {
|
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
|
||||||
|
|
||||||
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
|
|
||||||
s.lists["im.vector.fake.direct"].push(room);
|
|
||||||
} else {
|
|
||||||
s.lists["im.vector.fake.recent"].push(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save these new guessed DM rooms into the account data
|
|
||||||
const newMDirectEvent = {};
|
|
||||||
for (const room of s.lists["im.vector.fake.direct"]) {
|
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
|
||||||
const otherPerson = Rooms.getOnlyOtherMember(room, me);
|
|
||||||
if (!otherPerson) continue;
|
|
||||||
|
|
||||||
const roomList = newMDirectEvent[otherPerson.userId] || [];
|
|
||||||
roomList.push(room.roomId);
|
|
||||||
newMDirectEvent[otherPerson.userId] = roomList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this fails, fine, we'll just do the same thing next time we get the room lists
|
|
||||||
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
|
|
||||||
|
|
||||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||||
|
|
||||||
return s;
|
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
|
||||||
|
/*
|
||||||
|
this.listOrder = [
|
||||||
|
"im.vector.fake.invite",
|
||||||
|
"m.favourite",
|
||||||
|
"im.vector.fake.recent",
|
||||||
|
"im.vector.fake.direct",
|
||||||
|
Object.keys(otherTagNames).filter(tagName=>{
|
||||||
|
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
|
||||||
|
}).sort(),
|
||||||
|
"m.lowpriority",
|
||||||
|
"im.vector.fake.archived"
|
||||||
|
];
|
||||||
|
*/
|
||||||
|
|
||||||
|
return lists;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getScrollNode: function() {
|
_getScrollNode: function() {
|
||||||
var panel = ReactDOM.findDOMNode(this);
|
var panel = ReactDOM.findDOMNode(this);
|
||||||
if (!panel) return null;
|
if (!panel) return null;
|
||||||
|
|
||||||
if (panel.classList.contains('gm-prevented')) {
|
// empirically, if we have gm-prevented for some reason, the scroll node
|
||||||
return panel;
|
// is still the 3rd child (i.e. the view child). This looks to be due
|
||||||
} else {
|
// to vdh's improved resize updater logic...?
|
||||||
return panel.children[2]; // XXX: Fragile!
|
return panel.children[2]; // XXX: Fragile!
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_whenScrolling: function(e) {
|
_whenScrolling: function(e) {
|
||||||
|
@ -331,10 +455,11 @@ module.exports = React.createClass({
|
||||||
var incomingCallBox = document.getElementById("incomingCallBox");
|
var incomingCallBox = document.getElementById("incomingCallBox");
|
||||||
if (incomingCallBox && incomingCallBox.parentElement) {
|
if (incomingCallBox && incomingCallBox.parentElement) {
|
||||||
var scrollArea = this._getScrollNode();
|
var scrollArea = this._getScrollNode();
|
||||||
|
if (!scrollArea) return;
|
||||||
// Use the offset of the top of the scroll area from the window
|
// Use the offset of the top of the scroll area from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||||
// Use the offset of the top of the componet from the window
|
// Use the offset of the top of the component from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||||
|
|
||||||
|
@ -354,10 +479,11 @@ module.exports = React.createClass({
|
||||||
// properly through React
|
// properly through React
|
||||||
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
|
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
|
||||||
var scrollArea = this._getScrollNode();
|
var scrollArea = this._getScrollNode();
|
||||||
|
if (!scrollArea) return;
|
||||||
// Use the offset of the top of the scroll area from the window
|
// Use the offset of the top of the scroll area from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||||
// Use the offset of the top of the componet from the window
|
// Use the offset of the top of the component from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||||
|
|
||||||
|
@ -451,21 +577,74 @@ module.exports = React.createClass({
|
||||||
this.refs.gemscroll.forceUpdate();
|
this.refs.gemscroll.forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getEmptyContent: function(section) {
|
||||||
|
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
||||||
|
|
||||||
|
if (this.props.collapsed) {
|
||||||
|
return <RoomDropTarget label="" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
||||||
|
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||||
|
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||||
|
if (this.state.totalRoomCount === 0) {
|
||||||
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
|
switch (section) {
|
||||||
|
case 'im.vector.fake.direct':
|
||||||
|
return <div className="mx_RoomList_emptySubListTip">
|
||||||
|
Press
|
||||||
|
<StartChatButton size="16" />
|
||||||
|
to start a chat with someone
|
||||||
|
</div>;
|
||||||
|
case 'im.vector.fake.recent':
|
||||||
|
return <div className="mx_RoomList_emptySubListTip">
|
||||||
|
You're not in any rooms yet! Press
|
||||||
|
<CreateRoomButton size="16" />
|
||||||
|
to make a room or
|
||||||
|
<RoomDirectoryButton size="16" />
|
||||||
|
to browse the directory
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
|
||||||
|
|
||||||
|
return <RoomDropTarget label={labelText} />;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getHeaderItems: function(section) {
|
||||||
|
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
||||||
|
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||||
|
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||||
|
switch (section) {
|
||||||
|
case 'im.vector.fake.direct':
|
||||||
|
return <span className="mx_RoomList_headerButtons">
|
||||||
|
<StartChatButton size="16" />
|
||||||
|
</span>;
|
||||||
|
case 'im.vector.fake.recent':
|
||||||
|
return <span className="mx_RoomList_headerButtons">
|
||||||
|
<RoomDirectoryButton size="16" />
|
||||||
|
<CreateRoomButton size="16" />
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||||
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
|
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
|
||||||
<div className="mx_RoomList">
|
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||||
label="Invites"
|
label="Invites"
|
||||||
|
tagName="im.vector.fake.invite"
|
||||||
editable={ false }
|
editable={ false }
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
@ -473,51 +652,57 @@ module.exports = React.createClass({
|
||||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||||
label="Favourites"
|
label="Favourites"
|
||||||
tagName="m.favourite"
|
tagName="m.favourite"
|
||||||
verb="favourite"
|
emptyContent={this._getEmptyContent('m.favourite')}
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="manual"
|
order="manual"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
<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"
|
||||||
|
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
||||||
|
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||||
|
editable={ true }
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
alwaysShowHeader={ true }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||||
label="Rooms"
|
label="Rooms"
|
||||||
|
tagName="im.vector.fake.recent"
|
||||||
editable={ true }
|
editable={ true }
|
||||||
verb="restore"
|
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||||
|
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
{ Object.keys(self.state.lists).sort().map(function(tagName) {
|
||||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
return <RoomSubList list={ self.state.lists[tagName] }
|
return <RoomSubList list={ self.state.lists[tagName] }
|
||||||
key={ tagName }
|
key={ tagName }
|
||||||
label={ tagName }
|
label={ tagName }
|
||||||
tagName={ tagName }
|
tagName={ tagName }
|
||||||
verb={ "tag as " + tagName }
|
emptyContent={this._getEmptyContent(tagName)}
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="manual"
|
order="manual"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />;
|
onShowMoreRooms={ self.onShowMoreRooms } />;
|
||||||
|
@ -528,22 +713,23 @@ module.exports = React.createClass({
|
||||||
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||||
label="Low priority"
|
label="Low priority"
|
||||||
tagName="m.lowpriority"
|
tagName="m.lowpriority"
|
||||||
verb="demote"
|
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||||
label="Historical"
|
label="Historical"
|
||||||
|
tagName="im.vector.fake.archived"
|
||||||
editable={ false }
|
editable={ false }
|
||||||
order="recent"
|
order="recent"
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
alwaysShowHeader={ true }
|
alwaysShowHeader={ true }
|
||||||
startAsHidden={ true }
|
startAsHidden={ true }
|
||||||
showSpinner={ self.state.isLoadingLeftRooms }
|
showSpinner={ self.state.isLoadingLeftRooms }
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
@ -128,14 +129,17 @@ module.exports = React.createClass({
|
||||||
console.error("Failed to get room visibility: " + err);
|
console.error("Failed to get room visibility: " + err);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scalarClient = new ScalarAuthClient();
|
this.scalarClient = null;
|
||||||
this.scalarClient.connect().done(() => {
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
this.forceUpdate();
|
this.scalarClient = new ScalarAuthClient();
|
||||||
}, (err) => {
|
this.scalarClient.connect().done(() => {
|
||||||
this.setState({
|
this.forceUpdate();
|
||||||
scalar_error: err
|
}, (err) => {
|
||||||
|
this.setState({
|
||||||
|
scalar_error: err
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'ui_opacity',
|
action: 'ui_opacity',
|
||||||
|
@ -489,7 +493,7 @@ module.exports = React.createClass({
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
Modal.createDialog(IntegrationsManager, {
|
Modal.createDialog(IntegrationsManager, {
|
||||||
src: this.scalarClient.hasCredentials() ?
|
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||||
null,
|
null,
|
||||||
onFinished: ()=>{
|
onFinished: ()=>{
|
||||||
|
@ -764,36 +768,39 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var integrationsButton;
|
let integrationsButton;
|
||||||
var integrationsError;
|
let integrationsError;
|
||||||
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
|
||||||
console.error(this.state.scalar_error);
|
|
||||||
integrationsError = (
|
|
||||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
|
||||||
Could not connect to the integration server
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.scalarClient.hasCredentials()) {
|
if (this.scalarClient !== null) {
|
||||||
integrationsButton = (
|
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
||||||
|
console.error(this.state.scalar_error);
|
||||||
|
integrationsError = (
|
||||||
|
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||||
|
Could not connect to the integration server
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scalarClient.hasCredentials()) {
|
||||||
|
integrationsButton = (
|
||||||
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
|
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
|
||||||
Manage Integrations
|
Manage Integrations
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.scalar_error) {
|
} else if (this.state.scalar_error) {
|
||||||
integrationsButton = (
|
integrationsButton = (
|
||||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||||
{ integrationsError }
|
{ integrationsError }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
integrationsButton = (
|
integrationsButton = (
|
||||||
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
|
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
|
||||||
Manage Integrations
|
Manage Integrations
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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');
|
||||||
|
@ -28,6 +27,8 @@ var RoomNotifs = require('../../../RoomNotifs');
|
||||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
|
||||||
|
var Unread = require('../../../Unread');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
|
@ -35,13 +36,12 @@ 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,
|
||||||
|
selectedRoom: React.PropTypes.string,
|
||||||
|
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
selected: React.PropTypes.bool.isRequired,
|
|
||||||
unread: React.PropTypes.bool.isRequired,
|
|
||||||
highlight: React.PropTypes.bool.isRequired,
|
|
||||||
isInvite: React.PropTypes.bool.isRequired,
|
isInvite: React.PropTypes.bool.isRequired,
|
||||||
incomingCall: React.PropTypes.object,
|
incomingCall: React.PropTypes.object,
|
||||||
},
|
},
|
||||||
|
@ -54,11 +54,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
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),
|
||||||
|
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,32 +80,40 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onAccountData: function(accountDataEvent) {
|
|
||||||
if (accountDataEvent.getType() == 'm.push_rules') {
|
|
||||||
this.setState({
|
|
||||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
||||||
|
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
|
||||||
|
this.onRefresh();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
||||||
if (cli) {
|
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function() {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
dis.dispatch({
|
this.onRefresh();
|
||||||
action: 'view_room',
|
},
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
|
onRefresh: function(params) {
|
||||||
|
this.setState({
|
||||||
|
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
|
||||||
|
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSelect: function(params) {
|
||||||
|
this.setState({
|
||||||
|
selected: params.selected,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(ev) {
|
||||||
|
if (this.props.onClick) {
|
||||||
|
this.props.onClick(this.props.room.roomId, ev);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onMouseEnter: function() {
|
onMouseEnter: function() {
|
||||||
this.setState( { hover : true });
|
this.setState( { hover : true });
|
||||||
this.badgeOnMouseEnter();
|
this.badgeOnMouseEnter();
|
||||||
|
@ -137,62 +145,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];
|
||||||
|
@ -201,17 +179,17 @@ module.exports = React.createClass({
|
||||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
|
||||||
const badges = notifBadges || mentionBadges;
|
const badges = notifBadges || mentionBadges;
|
||||||
|
|
||||||
var classes = classNames({
|
var classes = classNames({
|
||||||
'mx_RoomTile': true,
|
'mx_RoomTile': true,
|
||||||
'mx_RoomTile_selected': this.props.selected,
|
'mx_RoomTile_selected': this.state.selected,
|
||||||
'mx_RoomTile_unread': this.props.unread,
|
'mx_RoomTile_unread': this.state.unread,
|
||||||
'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 +197,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 +210,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,10 +228,10 @@ 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.state.selected) {
|
||||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||||
|
|
||||||
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
||||||
|
@ -292,13 +265,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let ret = (
|
let ret = (
|
||||||
<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">
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li data-scroll-token={eventId+"+"+j}>
|
<li data-scroll-tokens={eventId+"+"+j}>
|
||||||
{ret}
|
{ret}
|
||||||
</li>);
|
</li>);
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
// cancel button which is shared between room header and simple room header
|
// cancel button which is shared between room header and simple room header
|
||||||
export function CancelButton(props) {
|
export function CancelButton(props) {
|
||||||
|
@ -45,6 +46,9 @@ export default React.createClass({
|
||||||
|
|
||||||
// is the RightPanel collapsed?
|
// is the RightPanel collapsed?
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// `src` to a TintableSvg. Optional.
|
||||||
|
icon: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
onShowRhsClick: function(ev) {
|
onShowRhsClick: function(ev) {
|
||||||
|
@ -53,9 +57,17 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let cancelButton;
|
let cancelButton;
|
||||||
|
let icon;
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||||
}
|
}
|
||||||
|
if (this.props.icon) {
|
||||||
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
|
icon = <TintableSvg
|
||||||
|
className="mx_RoomHeader_icon" src={this.props.icon}
|
||||||
|
width="25" height="25"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
let showRhsButton;
|
let showRhsButton;
|
||||||
/* // don't bother cluttering things up with this for now.
|
/* // don't bother cluttering things up with this for now.
|
||||||
|
@ -73,6 +85,7 @@ export default React.createClass({
|
||||||
<div className="mx_RoomHeader" >
|
<div className="mx_RoomHeader" >
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
<div className="mx_RoomHeader_simpleHeader">
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
|
{ icon }
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
{ showRhsButton }
|
{ showRhsButton }
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -32,10 +33,10 @@ module.exports = React.createClass({
|
||||||
<div className="mx_TopUnreadMessagesBar">
|
<div className="mx_TopUnreadMessagesBar">
|
||||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||||
onClick={this.props.onScrollUpClick}>
|
onClick={this.props.onScrollUpClick}>
|
||||||
<img src="img/scrollup.svg" width="24" height="24"
|
<img src="img/scrollto.svg" width="24" height="24"
|
||||||
alt="Scroll to unread messages"
|
alt="Scroll to unread messages"
|
||||||
title="Scroll to unread messages"/>
|
title="Scroll to unread messages"/>
|
||||||
Unread messages. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
Jump to first unread message.
|
||||||
</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"
|
||||||
|
|
173
src/components/views/settings/AddPhoneNumber.js
Normal file
173
src/components/views/settings/AddPhoneNumber.js
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
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.iso2 });
|
||||||
|
},
|
||||||
|
|
||||||
|
_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_UserSettings_phoneSection">
|
||||||
|
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
|
||||||
|
className="mx_UserSettings_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
isSmall={true}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}))
|
|
@ -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;
|
||||||
|
|
|
@ -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/(?:app|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
|
@ -115,7 +115,7 @@ var Tester = React.createClass({
|
||||||
//
|
//
|
||||||
// there is an extra 50 pixels of margin at the bottom.
|
// there is an extra 50 pixels of margin at the bottom.
|
||||||
return (
|
return (
|
||||||
<li key={key} data-scroll-token={key}>
|
<li key={key} data-scroll-tokens={key}>
|
||||||
<div style={{height: '98px', margin: '50px', border: '1px solid black',
|
<div style={{height: '98px', margin: '50px', border: '1px solid black',
|
||||||
backgroundColor: '#fff8dc' }}>
|
backgroundColor: '#fff8dc' }}>
|
||||||
{key}
|
{key}
|
||||||
|
|
|
@ -68,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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue