mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 09:46:09 +03:00
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)
|
||||
===================================================================================================
|
||||
[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: [
|
||||
// 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
|
||||
// cause stack overflows
|
||||
// (https://github.com/webpack/webpack/issues/1721), and
|
||||
// there is no need for webpack to parse them - they can
|
||||
// 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
|
||||
// tries to do voodoo with 'require' which upsets
|
||||
// webpack (https://github.com/webpack/webpack/issues/304)
|
||||
/sinon\/pkg\/sinon\.js$/,
|
||||
/sinon[\\\/]pkg[\\\/]sinon\.js$/,
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.8",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -32,8 +32,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"reskindex": "scripts/reskindex.js -h header",
|
||||
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
|
||||
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
|
||||
"build": "babel src -d lib --source-maps",
|
||||
"start": "babel src -w -d lib --source-maps",
|
||||
"lint": "eslint src/",
|
||||
"lintall": "eslint src/ test/",
|
||||
"clean": "rimraf lib",
|
||||
|
@ -53,7 +53,7 @@
|
|||
"draft-js-export-markdown": "^0.2.0",
|
||||
"emojione": "2.2.3",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "^3.1.2",
|
||||
"filesize": "3.5.6",
|
||||
"flux": "^2.0.3",
|
||||
"glob": "^5.0.14",
|
||||
"highlight.js": "^8.9.1",
|
||||
|
@ -63,11 +63,12 @@
|
|||
"lodash": "^4.13.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"optimist": "^0.6.1",
|
||||
"prop-types": "^15.5.8",
|
||||
"q": "^1.4.1",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-css-transition-group": "15.3.2",
|
||||
"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",
|
||||
"text-encoding-utf-8": "^1.0.1",
|
||||
"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 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -51,11 +52,36 @@ class AddThreepid {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to add a msisdn threepid. This will trigger a side-effect of
|
||||
* sending a test message to the provided phone number.
|
||||
* @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in
|
||||
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||
* @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server
|
||||
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||
*/
|
||||
addMsisdn(phoneCountry, phoneNumber, bind) {
|
||||
this.bind = bind;
|
||||
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||
).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||
err.message = "This phone number is already in use";
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email link has been clicked by attempting to add the threepid
|
||||
* @return {Promise} Resolves if the password was reset. Rejects with an object
|
||||
* @return {Promise} Resolves if the email address was added. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||
* the request failed.
|
||||
*/
|
||||
checkEmailLinkClicked() {
|
||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
|
@ -73,6 +99,29 @@ class AddThreepid {
|
|||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a phone number verification code as entered by the user and validates
|
||||
* it with the ID server, then if successful, adds the phone number.
|
||||
* @return {Promise} Resolves if the phone number was added. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the request failed.
|
||||
*/
|
||||
haveMsisdnToken(token) {
|
||||
return MatrixClientPeg.get().submitMsisdnToken(
|
||||
this.sessionId, this.clientSecret, token,
|
||||
).then((result) => {
|
||||
if (result.errcode) {
|
||||
throw result;
|
||||
}
|
||||
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
return MatrixClientPeg.get().addThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: identityServerDomain
|
||||
}, this.bind);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AddThreepid;
|
||||
|
|
|
@ -22,8 +22,8 @@ module.exports = {
|
|||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||
var url = member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod,
|
||||
false,
|
||||
false
|
||||
|
@ -40,7 +40,9 @@ module.exports = {
|
|||
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||
var url = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||
width, height, resizeMethod
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod
|
||||
);
|
||||
if (!url || url.length === 0) {
|
||||
return null;
|
||||
|
@ -57,4 +59,3 @@ module.exports = {
|
|||
return 'img/' + images[total % images.length] + '.png';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -82,4 +82,12 @@ export default class BasePlatform {
|
|||
screenCaptureErrorString() {
|
||||
return "Not implemented";
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the application, without neccessarily reloading
|
||||
* any application code
|
||||
*/
|
||||
reload() {
|
||||
throw new Error("reload not implemented!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,9 +310,10 @@ function _onAction(payload) {
|
|||
placeCall(call);
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Conference call failed: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set up conference call",
|
||||
description: "Conference call failed: " + err,
|
||||
description: "Conference call failed. " + ((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) {
|
||||
const content = {
|
||||
body: file.name,
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
size: file.size,
|
||||
}
|
||||
|
@ -316,7 +316,7 @@ class ContentMessages {
|
|||
}
|
||||
|
||||
const upload = {
|
||||
fileName: file.name,
|
||||
fileName: file.name || 'Attachment',
|
||||
roomId: roomId,
|
||||
total: 0,
|
||||
loaded: 0,
|
||||
|
|
|
@ -25,6 +25,9 @@ import emojione from 'emojione';
|
|||
import classNames from 'classnames';
|
||||
|
||||
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';
|
||||
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
|
@ -58,6 +61,29 @@ export function unicodeToImage(str) {
|
|||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one or more unicode characters (represented by unicode
|
||||
* character number), return an image node with the corresponding
|
||||
* emoji.
|
||||
*
|
||||
* @param alt {string} String to use for the image alt text
|
||||
* @param 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 {
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.innerHTML = html;
|
||||
|
@ -85,8 +111,7 @@ var sanitizeHtmlParams = {
|
|||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'del', // for markdown
|
||||
// deliberately no h1/h2 to stop people shouting.
|
||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
|
||||
],
|
||||
|
@ -98,6 +123,7 @@ var sanitizeHtmlParams = {
|
|||
// We don't currently allow img itself by default, but this
|
||||
// would make sense if we did
|
||||
img: ['src'],
|
||||
ol: ['start'],
|
||||
},
|
||||
// Lots of these won't come up by default because we don't allow them
|
||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
|
|
|
@ -32,4 +32,5 @@ module.exports = {
|
|||
DELETE: 46,
|
||||
KEY_D: 68,
|
||||
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
|
||||
* 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
|
||||
* 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
|
||||
* true; defines the IS to use.
|
||||
*
|
||||
* @returns {Promise} a promise which resolves when the above process completes.
|
||||
*/
|
||||
export function loadSession(opts) {
|
||||
const realQueryParams = opts.realQueryParams || {};
|
||||
|
@ -127,7 +128,7 @@ export function loadSession(opts) {
|
|||
|
||||
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||
// create a temporary MatrixClient to do the login
|
||||
var client = Matrix.createClient({
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: queryParams.homeserver,
|
||||
});
|
||||
|
||||
|
@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
|||
// Not really sure where the right home for it is.
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
var client = Matrix.createClient({
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
});
|
||||
|
||||
|
@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
|
|||
if (!localStorage) {
|
||||
return q(false);
|
||||
}
|
||||
const hs_url = localStorage.getItem("mx_hs_url");
|
||||
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
const access_token = localStorage.getItem("mx_access_token");
|
||||
const user_id = localStorage.getItem("mx_user_id");
|
||||
const device_id = localStorage.getItem("mx_device_id");
|
||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
|
||||
let is_guest;
|
||||
let isGuest;
|
||||
if (localStorage.getItem("mx_is_guest") !== null) {
|
||||
is_guest = localStorage.getItem("mx_is_guest") === "true";
|
||||
isGuest = localStorage.getItem("mx_is_guest") === "true";
|
||||
} else {
|
||||
// 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) {
|
||||
console.log("Restoring session for %s", user_id);
|
||||
if (accessToken && userId && hsUrl) {
|
||||
console.log("Restoring session for %s", userId);
|
||||
try {
|
||||
setLoggedIn({
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
accessToken: access_token,
|
||||
homeserverUrl: hs_url,
|
||||
identityServerUrl: is_url,
|
||||
guest: is_guest,
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
accessToken: accessToken,
|
||||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
guest: isGuest,
|
||||
});
|
||||
return q(true);
|
||||
} catch (e) {
|
||||
|
@ -273,9 +274,18 @@ export function initRtsClient(url) {
|
|||
*/
|
||||
export function setLoggedIn(credentials) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
||||
credentials.userId, credentials.guest,
|
||||
credentials.homeserverUrl);
|
||||
|
||||
console.log(
|
||||
"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
|
||||
let teamPromise = Promise.resolve(null);
|
||||
|
@ -347,7 +357,7 @@ export function logout() {
|
|||
return;
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||
MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||
(err) => {
|
||||
// Just throwing an error here is going to be very unhelpful
|
||||
// if you're trying to log out because your server's down and
|
||||
|
@ -358,8 +368,8 @@ export function logout() {
|
|||
// change your password).
|
||||
console.log("Failed to call logout API: token will not be invalidated");
|
||||
onLoggedOut();
|
||||
}
|
||||
);
|
||||
},
|
||||
).done();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -415,7 +425,7 @@ export function stopMatrixClient() {
|
|||
UserActivity.stop();
|
||||
Presence.stop();
|
||||
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.stopClient();
|
||||
cli.removeAllListeners();
|
||||
|
|
51
src/Login.js
51
src/Login.js
|
@ -105,21 +105,48 @@ export default class Login {
|
|||
});
|
||||
}
|
||||
|
||||
loginViaPassword(username, pass) {
|
||||
var self = this;
|
||||
var isEmail = username.indexOf("@") > 0;
|
||||
var loginParams = {
|
||||
password: pass,
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
};
|
||||
if (isEmail) {
|
||||
loginParams.medium = 'email';
|
||||
loginParams.address = username;
|
||||
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||
const self = this;
|
||||
|
||||
const isEmail = username.indexOf("@") > 0;
|
||||
|
||||
let identifier;
|
||||
let legacyParams; // parameters added to support old HSes
|
||||
if (phoneCountry && phoneNumber) {
|
||||
identifier = {
|
||||
type: 'm.id.phone',
|
||||
country: phoneCountry,
|
||||
number: phoneNumber,
|
||||
};
|
||||
// No legacy support for phone number login
|
||||
} else if (isEmail) {
|
||||
identifier = {
|
||||
type: 'm.id.thirdparty',
|
||||
medium: 'email',
|
||||
address: username,
|
||||
};
|
||||
legacyParams = {
|
||||
medium: 'email',
|
||||
address: username,
|
||||
};
|
||||
} else {
|
||||
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 q({
|
||||
homeserverUrl: self._hsUrl,
|
||||
|
|
|
@ -50,6 +50,18 @@ class MatrixClientPeg {
|
|||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
this.indexedDbWorkerScript = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the script href passed to the IndexedDB web worker
|
||||
* If set, a separate web worker will be started to run the IndexedDB
|
||||
* queries on.
|
||||
*
|
||||
* @param {string} script href to the script to be passed to the web worker
|
||||
*/
|
||||
setIndexedDbWorkerScript(script) {
|
||||
this.indexedDbWorkerScript = script;
|
||||
}
|
||||
|
||||
get(): MatrixClient {
|
||||
|
@ -125,12 +137,12 @@ class MatrixClientPeg {
|
|||
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
||||
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
||||
|
||||
opts.store = new Matrix.IndexedDBStore(
|
||||
new Matrix.IndexedDBStoreBackend(window.indexedDB, "riot-web-sync"),
|
||||
new Matrix.SyncAccumulator(), {
|
||||
localStorage: localStorage,
|
||||
}
|
||||
);
|
||||
opts.store = new Matrix.IndexedDBStore({
|
||||
indexedDB: window.indexedDB,
|
||||
dbName: "riot-web-sync",
|
||||
localStorage: localStorage,
|
||||
workerScript: this.indexedDbWorkerScript,
|
||||
});
|
||||
}
|
||||
|
||||
this.matrixClient = Matrix.createClient(opts);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,13 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var PlatformPeg = require("./PlatformPeg");
|
||||
var TextForEvent = require('./TextForEvent');
|
||||
var Avatar = require('./Avatar');
|
||||
var dis = require("./dispatcher");
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import TextForEvent from './TextForEvent';
|
||||
import Avatar from './Avatar';
|
||||
import dis from './dispatcher';
|
||||
import sdk from './index';
|
||||
import Modal from './Modal';
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -30,7 +31,7 @@ var dis = require("./dispatcher");
|
|||
* }
|
||||
*/
|
||||
|
||||
var Notifier = {
|
||||
const Notifier = {
|
||||
notifsByRoom: {},
|
||||
|
||||
notificationMessageForEvent: function(ev) {
|
||||
|
@ -49,16 +50,16 @@ var Notifier = {
|
|||
return;
|
||||
}
|
||||
|
||||
var msg = this.notificationMessageForEvent(ev);
|
||||
let msg = this.notificationMessageForEvent(ev);
|
||||
if (!msg) return;
|
||||
|
||||
var title;
|
||||
if (!ev.sender || room.name == ev.sender.name) {
|
||||
let title;
|
||||
if (!ev.sender || room.name === ev.sender.name) {
|
||||
title = room.name;
|
||||
// notificationMessageForEvent includes sender,
|
||||
// but we already have the sender here
|
||||
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
|
||||
// to display sender info
|
||||
title = room.name;
|
||||
|
@ -69,7 +70,7 @@ var Notifier = {
|
|||
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'
|
||||
) : null;
|
||||
|
||||
|
@ -84,7 +85,7 @@ var Notifier = {
|
|||
},
|
||||
|
||||
_playAudioNotification: function(ev, room) {
|
||||
var e = document.getElementById("messageAudio");
|
||||
const e = document.getElementById("messageAudio");
|
||||
if (e) {
|
||||
e.load();
|
||||
e.play();
|
||||
|
@ -96,19 +97,19 @@ var Notifier = {
|
|||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||
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);
|
||||
this.toolbarHidden = false;
|
||||
this.isPrepared = false;
|
||||
this.isSyncing = false;
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||
}
|
||||
this.isPrepared = false;
|
||||
this.isSyncing = false;
|
||||
},
|
||||
|
||||
supportsDesktopNotifications: function() {
|
||||
|
@ -122,7 +123,7 @@ var Notifier = {
|
|||
// make sure that we persist the current setting audio_enabled setting
|
||||
// before changing anything
|
||||
if (global.localStorage) {
|
||||
if(global.localStorage.getItem('audio_notifications_enabled') == null) {
|
||||
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
||||
this.setAudioEnabled(this.isEnabled());
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +133,16 @@ var Notifier = {
|
|||
plaf.requestNotificationPermission().done((result) => {
|
||||
if (result !== 'granted') {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -142,7 +153,7 @@ var Notifier = {
|
|||
if (callback) callback();
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: true
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
// clear the notifications_hidden flag, so that if notifications are
|
||||
|
@ -153,7 +164,7 @@ var Notifier = {
|
|||
global.localStorage.setItem('notifications_enabled', 'false');
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: false
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -166,7 +177,7 @@ var Notifier = {
|
|||
|
||||
if (!global.localStorage) return true;
|
||||
|
||||
var enabled = global.localStorage.getItem('notifications_enabled');
|
||||
const enabled = global.localStorage.getItem('notifications_enabled');
|
||||
if (enabled === null) return true;
|
||||
return enabled === 'true';
|
||||
},
|
||||
|
@ -174,12 +185,12 @@ var Notifier = {
|
|||
setAudioEnabled: function(enable) {
|
||||
if (!global.localStorage) return;
|
||||
global.localStorage.setItem('audio_notifications_enabled',
|
||||
enable ? 'true' : 'false');
|
||||
enable ? 'true' : 'false');
|
||||
},
|
||||
|
||||
isAudioEnabled: function(enable) {
|
||||
if (!global.localStorage) return true;
|
||||
var enabled = global.localStorage.getItem(
|
||||
const enabled = global.localStorage.getItem(
|
||||
'audio_notifications_enabled');
|
||||
// default to true if the popups are enabled
|
||||
if (enabled === null) return this.isEnabled();
|
||||
|
@ -193,7 +204,7 @@ var Notifier = {
|
|||
// this is nothing to do with notifier_enabled
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: this.isEnabled()
|
||||
value: this.isEnabled(),
|
||||
});
|
||||
|
||||
// update the info to localStorage for persistent settings
|
||||
|
@ -214,22 +225,21 @@ var Notifier = {
|
|||
},
|
||||
|
||||
onSyncStateChange: function(state) {
|
||||
if (state === "PREPARED" || state === "SYNCING") {
|
||||
this.isPrepared = true;
|
||||
}
|
||||
else if (state === "STOPPED" || state === "ERROR") {
|
||||
this.isPrepared = false;
|
||||
if (state === "SYNCING") {
|
||||
this.isSyncing = true;
|
||||
} else if (state === "STOPPED" || state === "ERROR") {
|
||||
this.isSyncing = false;
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (toStartOfTimeline) return;
|
||||
if (!room) return;
|
||||
if (!this.isPrepared) return; // don't alert for any messages initially
|
||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
|
||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
|
||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
if (actions && actions.notify) {
|
||||
if (this.isEnabled()) {
|
||||
this._displayPopupNotification(ev, room);
|
||||
|
@ -241,7 +251,7 @@ var Notifier = {
|
|||
},
|
||||
|
||||
onRoomReceipt: function(ev, room) {
|
||||
if (room.getUnreadNotificationCount() == 0) {
|
||||
if (room.getUnreadNotificationCount() === 0) {
|
||||
// ideally we would clear each notification when it was read,
|
||||
// but we have no way, given a read receipt, to know whether
|
||||
// the receipt comes before or after an event, so we can't
|
||||
|
@ -256,7 +266,7 @@ var Notifier = {
|
|||
}
|
||||
delete this.notifsByRoom[room.roomId];
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function guessAndSetDMRoom(room, isDirect) {
|
||||
let newTarget;
|
||||
if (isDirect) {
|
||||
const guessedTarget = guessDMRoomTarget(
|
||||
room, room.getMember(MatrixClientPeg.get().credentials.userId),
|
||||
);
|
||||
newTarget = guessedTarget.userId;
|
||||
} else {
|
||||
newTarget = null;
|
||||
}
|
||||
|
||||
return setDMRoom(room.roomId, newTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks or unmarks the given room as being as a DM room.
|
||||
* @param {string} roomId The ID of the room to modify
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var CallHandler = require("./CallHandler");
|
||||
|
||||
import * as Roles from './Roles';
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
|
@ -63,8 +65,8 @@ function textForMemberEvent(ev) {
|
|||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
||||
return senderName + " set a profile picture";
|
||||
} else {
|
||||
// hacky hack for https://github.com/vector-im/vector-web/issues/2020
|
||||
return senderName + " rejoined the room.";
|
||||
// suppress null rejoins
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||
|
@ -116,7 +118,6 @@ function textForRoomNameEvent(ev) {
|
|||
|
||||
function textForMessageEvent(ev) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
|
@ -183,6 +184,45 @@ function textForEncryptionEvent(event) {
|
|||
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")";
|
||||
}
|
||||
|
||||
// Currently will only display a change if a user's power level is changed
|
||||
function textForPowerEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
if (!event.getPrevContent() || !event.getPrevContent().users) {
|
||||
return '';
|
||||
}
|
||||
const userDefault = event.getContent().users_default || 0;
|
||||
// Construct set of userIds
|
||||
let users = [];
|
||||
Object.keys(event.getContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
);
|
||||
Object.keys(event.getPrevContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
);
|
||||
let diff = [];
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
const from = event.getPrevContent().users[userId];
|
||||
// Current power level
|
||||
const to = event.getContent().users[userId];
|
||||
if (to !== from) {
|
||||
diff.push(
|
||||
userId +
|
||||
' from ' + Roles.textualPowerLevel(from, userDefault) +
|
||||
' to ' + Roles.textualPowerLevel(to, userDefault)
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!diff.length) {
|
||||
return '';
|
||||
}
|
||||
return senderName + ' changed the power level of ' + diff.join(', ');
|
||||
}
|
||||
|
||||
var handlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
|
@ -194,6 +234,7 @@ var handlers = {
|
|||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.encryption': textForEncryptionEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -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 sdk from './index';
|
||||
import Modal from './Modal';
|
||||
|
||||
let isDialogOpen = false;
|
||||
|
||||
const onAction = function(payload) {
|
||||
if (payload.action === 'unknown_device_error') {
|
||||
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
isDialogOpen = true;
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: payload.err.devices,
|
||||
room: payload.room,
|
||||
onFinished: (r) => {
|
||||
isDialogOpen = false;
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
|
|
|
@ -32,7 +32,7 @@ class UserActivity {
|
|||
start() {
|
||||
document.onmousedown = 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
|
||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||
// also this needs to be the wheel event, not scroll, as scroll is
|
||||
|
@ -50,7 +50,7 @@ class UserActivity {
|
|||
stop() {
|
||||
document.onmousedown = undefined;
|
||||
document.onmousemove = undefined;
|
||||
document.onkeypress = undefined;
|
||||
document.onkeydown = undefined;
|
||||
window.removeEventListener('wheel', this._onUserActivity.bind(this),
|
||||
{ passive: true, capture: true });
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
var q = require("q");
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var Notifier = require("./Notifier");
|
||||
import q from 'q';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import Notifier from './Notifier';
|
||||
|
||||
/*
|
||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||
|
@ -33,7 +33,7 @@ module.exports = {
|
|||
],
|
||||
|
||||
loadProfileInfo: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId);
|
||||
},
|
||||
|
||||
|
@ -44,7 +44,7 @@ module.exports = {
|
|||
loadThreePids: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return q({
|
||||
threepids: []
|
||||
threepids: [],
|
||||
}); // guests can't poke 3pid endpoint
|
||||
}
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
|
@ -73,19 +73,19 @@ module.exports = {
|
|||
Notifier.setAudioEnabled(enable);
|
||||
},
|
||||
|
||||
changePassword: function(old_password, new_password) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
changePassword: function(oldPassword, newPassword) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
var authDict = {
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
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
|
||||
* email address. Email pushers all have the same app ID, so since
|
||||
* pushers are unique over (app ID, pushkey), there will be at most
|
||||
|
@ -95,8 +95,8 @@ module.exports = {
|
|||
if (pushers === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
for (var i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
||||
return pushers[i];
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ module.exports = {
|
|||
addEmailPusher: function(address, data) {
|
||||
return MatrixClientPeg.get().setPusher({
|
||||
kind: 'email',
|
||||
app_id: "m.email",
|
||||
app_id: 'm.email',
|
||||
pushkey: address,
|
||||
app_display_name: 'Email Notifications',
|
||||
device_display_name: address,
|
||||
|
@ -121,46 +121,46 @@ module.exports = {
|
|||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
setUrlPreviewsDisabled: function(disabled) {
|
||||
// FIXME: handle errors
|
||||
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
|
||||
disable: disabled
|
||||
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
|
||||
disable: disabled,
|
||||
});
|
||||
},
|
||||
|
||||
getSyncedSettings: function() {
|
||||
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
|
||||
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
|
||||
return event ? event.getContent() : {};
|
||||
},
|
||||
|
||||
getSyncedSetting: function(type, defaultValue = null) {
|
||||
var settings = this.getSyncedSettings();
|
||||
const settings = this.getSyncedSettings();
|
||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||
},
|
||||
|
||||
setSyncedSetting: function(type, value) {
|
||||
var settings = this.getSyncedSettings();
|
||||
const settings = this.getSyncedSettings();
|
||||
settings[type] = value;
|
||||
// FIXME: handle errors
|
||||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
||||
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
|
||||
},
|
||||
|
||||
getLocalSettings: function() {
|
||||
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||
return JSON.parse(localSettingsString);
|
||||
},
|
||||
|
||||
getLocalSetting: function(type, defaultValue = null) {
|
||||
var settings = this.getLocalSettings();
|
||||
const settings = this.getLocalSettings();
|
||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||
},
|
||||
|
||||
setLocalSetting: function(type, value) {
|
||||
var settings = this.getLocalSettings();
|
||||
const settings = this.getLocalSettings();
|
||||
settings[type] = value;
|
||||
// FIXME: handle errors
|
||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||
|
@ -171,8 +171,8 @@ module.exports = {
|
|||
if (MatrixClientPeg.get().isGuest()) return false;
|
||||
|
||||
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
||||
for (var i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||
var f = this.LABS_FEATURES[i];
|
||||
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||
const f = this.LABS_FEATURES[i];
|
||||
if (f.id === feature) {
|
||||
return f.default;
|
||||
}
|
||||
|
@ -183,5 +183,5 @@ module.exports = {
|
|||
|
||||
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
||||
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);
|
||||
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
|
||||
import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
|
||||
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
|
||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||
import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
|
||||
views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
|
||||
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
|
||||
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
||||
|
@ -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);
|
||||
import views$elements$AccessibleButton from './components/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';
|
||||
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
||||
import views$elements$AddressTile from './components/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';
|
||||
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
||||
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
||||
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
||||
import views$elements$Dropdown from './components/views/elements/Dropdown';
|
||||
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
|
||||
import views$elements$EditableText from './components/views/elements/EditableText';
|
||||
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
||||
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
||||
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
|
||||
import views$elements$EmojiText from './components/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';
|
||||
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
|
||||
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
|
||||
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
|
||||
import views$elements$ProgressBar from './components/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';
|
||||
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
|
||||
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);
|
||||
import views$login$CasLogin from './components/views/login/CasLogin';
|
||||
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
||||
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
|
||||
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
|
||||
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
||||
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
||||
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
||||
|
@ -221,6 +241,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread
|
|||
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
|
||||
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
||||
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
|
||||
import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
|
||||
views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
|
||||
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
|
||||
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
||||
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
||||
|
|
|
@ -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({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
return makeRequestPromise.finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -81,6 +82,13 @@ export default React.createClass({
|
|||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.roomView.canResetTimeline();
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -99,9 +107,21 @@ export default React.createClass({
|
|||
var handled = false;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
|
||||
// Implemented this way so possible handling for other pages is neater
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.UserSettings:
|
||||
this.props.onUserSettingsClose();
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey) {
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
var action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
|
@ -111,13 +131,15 @@ export default React.createClass({
|
|||
|
||||
case KeyCode.PAGE_UP:
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -135,22 +157,25 @@ export default React.createClass({
|
|||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
}
|
||||
else if (this.refs.roomDirectory) {
|
||||
this.refs.roomDirectory.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
var RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
var RoomView = sdk.getComponent('structures.RoomView');
|
||||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
var HomePage = sdk.getComponent('structures.HomePage');
|
||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
||||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
const HomePage = sdk.getComponent('structures.HomePage');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
|
||||
var page_element;
|
||||
var right_panel = '';
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
|
@ -195,10 +220,9 @@ export default React.createClass({
|
|||
|
||||
case PageTypes.RoomDirectory:
|
||||
page_element = <RoomDirectory
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
ref="roomDirectory"
|
||||
config={this.props.config.roomDirectory}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
|
|
|
@ -29,10 +29,6 @@ var UserActivity = require("../../UserActivity");
|
|||
var Presence = require("../../Presence");
|
||||
var dis = require("../../dispatcher");
|
||||
|
||||
var Login = require("./login/Login");
|
||||
var Registration = require("./login/Registration");
|
||||
var PostRegistration = require("./login/PostRegistration");
|
||||
|
||||
var Modal = require("../../Modal");
|
||||
var Tinter = require("../../Tinter");
|
||||
var sdk = require('../../index');
|
||||
|
@ -63,6 +59,13 @@ module.exports = React.createClass({
|
|||
// called when the session load completes
|
||||
onLoadCompleted: React.PropTypes.func,
|
||||
|
||||
// Represents the screen to display as a result of parsing the initial
|
||||
// window.location
|
||||
initialScreenAfterLogin: React.PropTypes.shape({
|
||||
screen: React.PropTypes.string.isRequired,
|
||||
params: React.PropTypes.object,
|
||||
}),
|
||||
|
||||
// displayname, if any, to set on the device when logging
|
||||
// in/registering.
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
|
@ -89,6 +92,12 @@ module.exports = React.createClass({
|
|||
var s = {
|
||||
loading: true,
|
||||
screen: undefined,
|
||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
||||
|
||||
// Stashed guest credentials if the user logs out
|
||||
// whilst logged in as a guest user (so they can change
|
||||
// their mind & log back in)
|
||||
guestCreds: null,
|
||||
|
||||
// What the LoggedInView would be showing if visible
|
||||
page_type: null,
|
||||
|
@ -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
|
||||
viewUserId: null,
|
||||
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
loggingIn: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
ready: false,
|
||||
|
@ -184,13 +194,9 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
// Stashed guest credentials if the user logs out
|
||||
// whilst logged in as a guest user (so they can change
|
||||
// their mind & log back in)
|
||||
this.guestCreds = null;
|
||||
|
||||
// if the automatic session load failed, the error
|
||||
this.sessionLoadError = null;
|
||||
// Used by _viewRoom before getting state from sync
|
||||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = q.defer();
|
||||
|
||||
if (this.props.config.sync_timeline_limit) {
|
||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
|
@ -280,7 +286,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
}).catch((e) => {
|
||||
console.error("Unable to load session", e);
|
||||
this.sessionLoadError = e.message;
|
||||
}).done(()=>{
|
||||
// stuff this through the dispatcher so that it happens
|
||||
// after the on_logged_in action.
|
||||
|
@ -307,7 +312,7 @@ module.exports = React.createClass({
|
|||
const newState = {
|
||||
screen: undefined,
|
||||
viewUserId: null,
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
ready: false,
|
||||
upgradeUsername: null,
|
||||
guestAccessToken: null,
|
||||
|
@ -317,14 +322,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
var self = this;
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
}
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'start_registration':
|
||||
|
@ -344,14 +348,20 @@ module.exports = React.createClass({
|
|||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_login':
|
||||
if (this.state.logged_in) return;
|
||||
if (MatrixClientPeg.get() &&
|
||||
MatrixClientPeg.get().isGuest()
|
||||
) {
|
||||
this.setState({
|
||||
guestCreds: MatrixClientPeg.getCredentials(),
|
||||
});
|
||||
}
|
||||
this.setStateForNewScreen({
|
||||
screen: 'login',
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({ // don't clobber logged_in status
|
||||
this.setState({ // don't clobber loggedIn status
|
||||
screen: 'post_registration'
|
||||
});
|
||||
break;
|
||||
|
@ -359,8 +369,8 @@ module.exports = React.createClass({
|
|||
// also stash our credentials, then if we restore the session,
|
||||
// we can just do it the same way whether we started upgrade
|
||||
// registration or explicitly logged out
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
this.setStateForNewScreen({
|
||||
guestCreds: MatrixClientPeg.getCredentials(),
|
||||
screen: "register",
|
||||
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
|
||||
|
@ -375,35 +385,60 @@ module.exports = React.createClass({
|
|||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
if (this.state.logged_in) return;
|
||||
if (this.state.loggedIn) return;
|
||||
this.setStateForNewScreen({
|
||||
screen: 'forgot_password',
|
||||
});
|
||||
this.notifyNewScreen('forgot_password');
|
||||
break;
|
||||
case 'leave_room':
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
var roomId = payload.room_id;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Leave room",
|
||||
description: "Are you sure you want to leave the room?",
|
||||
onFinished: function(should_leave) {
|
||||
onFinished: (should_leave) => {
|
||||
if (should_leave) {
|
||||
var d = MatrixClientPeg.get().leave(roomId);
|
||||
const d = MatrixClientPeg.get().leave(payload.room_id);
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.then(function() {
|
||||
d.then(() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}, function(err) {
|
||||
if (this.currentRoomId === payload.room_id) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + payload.room_id + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to leave room",
|
||||
description: (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()
|
||||
});
|
||||
});
|
||||
|
@ -530,6 +565,9 @@ module.exports = React.createClass({
|
|||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
case 'on_logging_in':
|
||||
this.setState({loggingIn: true});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
this._onLoggedIn(payload.teamToken);
|
||||
break;
|
||||
|
@ -603,36 +641,38 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (this.sdkReady) {
|
||||
// if the SDK is not ready yet, remember what room
|
||||
// we're supposed to be on but don't notify about
|
||||
// the new screen yet (we won't be showing it yet)
|
||||
// The normal case where this happens is navigating
|
||||
// to the room in the URL bar on page load.
|
||||
var presentedId = room_info.room_alias || room_info.room_id;
|
||||
var room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
||||
// Wait for the first sync to complete so that if a room does have an alias,
|
||||
// it would have been retrieved.
|
||||
let waitFor = q(null);
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a room before first sync. room_id:', room_info.room_id);
|
||||
return;
|
||||
}
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
waitFor.done(() => {
|
||||
let presentedId = room_info.room_alias || room_info.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
||||
if (room) {
|
||||
var theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
const theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
|
||||
// No need to do this given RoomView triggers it itself...
|
||||
// var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
// var color_scheme = {};
|
||||
// if (color_scheme_event) {
|
||||
// color_scheme = color_scheme_event.getContent();
|
||||
// // XXX: we should validate the event
|
||||
// }
|
||||
// console.log("Tinter.tint from _viewRoom");
|
||||
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
// persist which room is being stored across refreshes and browser quits.
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_last_room_id', room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
if (room_info.event_id) {
|
||||
presentedId += "/"+room_info.event_id;
|
||||
presentedId += "/" + room_info.event_id;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
this.notifyNewScreen('room/' + presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
this.setState(newState);
|
||||
});
|
||||
},
|
||||
|
||||
_createChat: function() {
|
||||
|
@ -658,6 +698,14 @@ module.exports = React.createClass({
|
|||
_onLoadCompleted: function() {
|
||||
this.props.onLoadCompleted();
|
||||
this.setState({loading: false});
|
||||
|
||||
// Show screens (like 'register') that need to be shown without _onLoggedIn
|
||||
// being called. 'register' needs to be routed here when the email confirmation
|
||||
// link is clicked on.
|
||||
if (this.state.screenAfterLogin &&
|
||||
['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) {
|
||||
this._showScreenAfterLogin();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -708,18 +756,46 @@ module.exports = React.createClass({
|
|||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: function(teamToken) {
|
||||
this.guestCreds = null;
|
||||
this.notifyNewScreen('');
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
logged_in: true,
|
||||
guestCreds: null,
|
||||
loggedIn: true,
|
||||
loggingIn: false,
|
||||
});
|
||||
|
||||
if (teamToken) {
|
||||
// A team member has logged in, not a guest
|
||||
this._teamToken = teamToken;
|
||||
this._setPage(PageTypes.HomePage);
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} 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() {
|
||||
this.notifyNewScreen('login');
|
||||
this.setStateForNewScreen({
|
||||
logged_in: false,
|
||||
loggedIn: false,
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
|
@ -745,9 +821,31 @@ module.exports = React.createClass({
|
|||
* (useful for setting listeners)
|
||||
*/
|
||||
_onWillStartClient() {
|
||||
var self = this;
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var self = this;
|
||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||
// memory consumed as the JS SDK stores multiple distinct copies of room
|
||||
// state (each of which can be 10s of MBs) for each DISJOINT timeline. This is
|
||||
// particularly noticeable when there are lots of 'limited' /sync responses
|
||||
// such as when laptops unsleep.
|
||||
// https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568
|
||||
cli.setCanResetTimelineCallback(function(roomId) {
|
||||
console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId);
|
||||
if (roomId !== self.state.currentRoomId) {
|
||||
// It is safe to remove events from rooms we are not viewing.
|
||||
return true;
|
||||
}
|
||||
// We are viewing the room which we want to reset. It is only safe to do
|
||||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self.refs.loggedInView) {
|
||||
return true;
|
||||
}
|
||||
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
self.updateStatusIndicator(state, prevState);
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
|
@ -755,55 +853,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
console.log("MatrixClient sync state => %s", state);
|
||||
if (state !== "PREPARED") { return; }
|
||||
self.sdkReady = true;
|
||||
|
||||
if (self.starting_room_alias_payload) {
|
||||
dis.dispatch(self.starting_room_alias_payload);
|
||||
delete self.starting_room_alias_payload;
|
||||
} else if (!self.state.page_type) {
|
||||
if (!self.state.currentRoomId) {
|
||||
var firstRoom = null;
|
||||
if (cli.getRooms() && cli.getRooms().length) {
|
||||
firstRoom = RoomListSorter.mostRecentActivityFirst(
|
||||
cli.getRooms()
|
||||
)[0].roomId;
|
||||
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
|
||||
} else {
|
||||
if (self._teamToken) {
|
||||
self.setState({ready: true, page_type: PageTypes.HomePage});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomView});
|
||||
}
|
||||
self.firstSyncComplete = true;
|
||||
self.firstSyncPromise.resolve();
|
||||
|
||||
// we notifyNewScreen now because now the room will actually be displayed,
|
||||
// and (mostly) now we can get the correct alias.
|
||||
var presentedId = self.state.currentRoomId;
|
||||
var room = MatrixClientPeg.get().getRoom(self.state.currentRoomId);
|
||||
if (room) {
|
||||
var theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
|
||||
if (presentedId != undefined) {
|
||||
self.notifyNewScreen('room/'+presentedId);
|
||||
} else {
|
||||
// There is no information on presentedId
|
||||
// so point user to fallback like /directory
|
||||
if (self._teamToken) {
|
||||
self.notifyNewScreen('home');
|
||||
} else {
|
||||
self.notifyNewScreen('directory');
|
||||
}
|
||||
}
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
} else {
|
||||
self.setState({ready: true});
|
||||
}
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
self.setState({ready: true});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
dis.dispatch({
|
||||
|
@ -903,12 +958,7 @@ module.exports = React.createClass({
|
|||
|
||||
// we can't view a room unless we're logged in
|
||||
// (a guest account is fine)
|
||||
if (!this.state.logged_in) {
|
||||
// we may still be loading (ie, trying to register a guest
|
||||
// session); otherwise we're (probably) already showing a login
|
||||
// screen. Either way, we'll show the room once the client starts.
|
||||
this.starting_room_alias_payload = payload;
|
||||
} else {
|
||||
if (this.state.loggedIn) {
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
} else if (screen.indexOf('user/') == 0) {
|
||||
|
@ -1002,9 +1052,9 @@ module.exports = React.createClass({
|
|||
|
||||
onReturnToGuestClick: function() {
|
||||
// reanimate our guest login
|
||||
if (this.guestCreds) {
|
||||
Lifecycle.setLoggedIn(this.guestCreds);
|
||||
this.guestCreds = null;
|
||||
if (this.state.guestCreds) {
|
||||
Lifecycle.setLoggedIn(this.state.guestCreds);
|
||||
this.setState({guestCreds: null});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1086,14 +1136,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
|
||||
// console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
||||
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
||||
|
||||
if (this.state.loading) {
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
// `loading` might be set to false before `loggedIn = true`, causing the default
|
||||
// (`<Login>`) to be visible for a few MS (say, whilst a request is in-flight to
|
||||
// the RTS). So in the meantime, use `loggingIn`, which is true between
|
||||
// actions `on_logging_in` and `on_logged_in`.
|
||||
if (this.state.loading || this.state.loggingIn) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
|
@ -1102,15 +1150,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
else if (this.state.screen == 'post_registration') {
|
||||
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
||||
return (
|
||||
<PostRegistration
|
||||
onComplete={this.onFinishPostRegistration} />
|
||||
);
|
||||
} else if (this.state.logged_in && this.state.ready) {
|
||||
} else if (this.state.loggedIn && this.state.ready) {
|
||||
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
||||
* we should go through and figure out what we actually need to pass down, as well
|
||||
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||
onRoomIdResolved={this.onRoomIdResolved}
|
||||
|
@ -1121,9 +1171,9 @@ module.exports = React.createClass({
|
|||
{...this.state}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.logged_in) {
|
||||
} else if (this.state.loggedIn) {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
|
@ -1133,6 +1183,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
const Registration = sdk.getComponent('structures.login.Registration');
|
||||
return (
|
||||
<Registration
|
||||
clientSecret={this.state.register_client_secret}
|
||||
|
@ -1153,10 +1204,11 @@ module.exports = React.createClass({
|
|||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.screen == 'forgot_password') {
|
||||
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
return (
|
||||
<ForgotPassword
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
|
@ -1168,7 +1220,8 @@ module.exports = React.createClass({
|
|||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
var r = (
|
||||
const Login = sdk.getComponent('structures.login.Login');
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
|
@ -1180,17 +1233,9 @@ module.exports = React.createClass({
|
|||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
initialErrorText={this.sessionLoadError}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
|
||||
// we only want to show the session load error the first time the
|
||||
// Login component is rendered. This is pretty hacky but I can't
|
||||
// think of another way to achieve it.
|
||||
this.sessionLoadError = null;
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -279,23 +279,25 @@ module.exports = React.createClass({
|
|||
this.currentGhostEventId = null;
|
||||
}
|
||||
|
||||
var isMembershipChange = (e) =>
|
||||
e.getType() === 'm.room.member'
|
||||
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
||||
var isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
var mxEv = this.props.events[i];
|
||||
var wantTile = true;
|
||||
var eventId = mxEv.getId();
|
||||
let mxEv = this.props.events[i];
|
||||
let wantTile = true;
|
||||
let eventId = mxEv.getId();
|
||||
let readMarkerInMels = false;
|
||||
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
wantTile = false;
|
||||
}
|
||||
|
||||
var last = (i == lastShownEventIndex);
|
||||
let last = (i == lastShownEventIndex);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
|
||||
if (isMembershipChange(mxEv) &&
|
||||
EventTile.haveTileForEvent(mxEv) &&
|
||||
!mxEv.isRedacted()
|
||||
) {
|
||||
let ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
|
@ -331,6 +333,9 @@ module.exports = React.createClass({
|
|||
|
||||
let eventTiles = summarisedEvents.map(
|
||||
(e) => {
|
||||
if (e.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
|
@ -349,12 +354,16 @@ module.exports = React.createClass({
|
|||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
data-scroll-token={eventId}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -385,6 +394,8 @@ module.exports = React.createClass({
|
|||
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 we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
@ -408,7 +419,9 @@ module.exports = React.createClass({
|
|||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()) {
|
||||
continuation = true;
|
||||
|
@ -459,8 +472,9 @@ module.exports = React.createClass({
|
|||
ret.push(
|
||||
<li key={eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-token={scrollToken}>
|
||||
data-scroll-tokens={scrollToken}>
|
||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
onWidgetLoad={this._onWidgetLoad}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
|
@ -481,13 +495,17 @@ module.exports = React.createClass({
|
|||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare weekdays
|
||||
return prevEvent.getDate().getDay() !== nextEventDate.getDay();
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
|
|
|
@ -96,26 +96,12 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
|
||||
this._checkSize();
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
||||
this.props.onResize();
|
||||
}
|
||||
|
||||
const size = this._getSize(this.props, this.state);
|
||||
if (size > 0) {
|
||||
this.props.onVisible();
|
||||
} else {
|
||||
if (this.hideDebouncer) {
|
||||
clearTimeout(this.hideDebouncer);
|
||||
}
|
||||
this.hideDebouncer = setTimeout(() => {
|
||||
// temporarily stop hiding the statusbar as per
|
||||
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
|
||||
// this.props.onHidden();
|
||||
}, HIDE_DEBOUNCE_MS);
|
||||
}
|
||||
componentDidUpdate: function() {
|
||||
this._checkSize();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -142,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
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function(props, state) {
|
||||
if (state.syncState === "ERROR" ||
|
||||
(state.usersTyping.length > 0) ||
|
||||
props.numUnreadMessages ||
|
||||
!props.atEndOfLiveTimeline ||
|
||||
props.hasActiveCall ||
|
||||
props.tabComplete.isTabCompleting()
|
||||
_getSize: function() {
|
||||
if (this.state.syncState === "ERROR" ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall ||
|
||||
this.props.tabComplete.isTabCompleting()
|
||||
) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (props.tabCompleteEntries) {
|
||||
} else if (this.props.tabCompleteEntries) {
|
||||
return STATUS_BAR_HIDDEN;
|
||||
} else if (props.unsentMessageError) {
|
||||
} else if (this.props.unsentMessageError) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
},
|
||||
|
||||
// determine if we need to call onResize
|
||||
_checkForResize: function(prevProps, prevState) {
|
||||
// figure out the old height and the new height of the status bar.
|
||||
return this._getSize(prevProps, prevState)
|
||||
!== this._getSize(this.props, this.state);
|
||||
},
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
//
|
||||
// if wantPlaceholder is true, we include a "..." placeholder if
|
||||
|
|
|
@ -26,6 +26,7 @@ var q = require("q");
|
|||
var classNames = require("classnames");
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var ContentMessages = require("../../ContentMessages");
|
||||
var Modal = require("../../Modal");
|
||||
|
@ -270,6 +271,7 @@ module.exports = React.createClass({
|
|||
|
||||
this._updateConfCallNotification();
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
|
@ -352,6 +354,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
|
@ -364,6 +367,17 @@ module.exports = React.createClass({
|
|||
// 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) {
|
||||
let handled = false;
|
||||
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,
|
||||
// after a successful peek, or after we join the room).
|
||||
_onRoomLoaded: function(room) {
|
||||
|
@ -914,8 +935,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
uploadFile: function(file) {
|
||||
var self = this;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
|
@ -927,11 +946,20 @@ module.exports = React.createClass({
|
|||
|
||||
ContentMessages.sendContentToRoom(
|
||||
file, this.state.room.roomId, MatrixClientPeg.get()
|
||||
).done(undefined, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
).done(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: error,
|
||||
room: this.state.room,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to upload file " + file + " " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to upload file",
|
||||
description: error.toString()
|
||||
description: ((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) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Search failed: " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Search failed",
|
||||
description: error.toString()
|
||||
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("),
|
||||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
|
@ -1165,6 +1194,7 @@ module.exports = React.createClass({
|
|||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
this.setState({editingRoomSettings: false});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -1238,6 +1268,7 @@ module.exports = React.createClass({
|
|||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this.refs.messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
|
@ -1257,12 +1288,7 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var pos = this.refs.messagePanel.getReadMarkerPosition();
|
||||
|
||||
// we want to show the bar if the read-marker is off the top of the
|
||||
// screen.
|
||||
var showBar = (pos < 0);
|
||||
|
||||
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||
this.setState({showTopUnreadMessagesBar: showBar},
|
||||
this.onChildResize);
|
||||
|
@ -1701,7 +1727,7 @@ module.exports = React.createClass({
|
|||
var messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
manageReadReceipts={true}
|
||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
manageReadMarkers={true}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
|||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 3000;
|
||||
const UNPAGINATION_PADDING = 6000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
|
|||
* 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.
|
||||
*
|
||||
* Each child element should have a 'data-scroll-token'. This token is used to
|
||||
* serialise the scroll state, and returned as the 'trackedScrollToken'
|
||||
* attribute by getScrollState().
|
||||
* Each child element should have a 'data-scroll-tokens'. This string of
|
||||
* comma-separated tokens may contain a single token or many, where many indicates
|
||||
* 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:
|
||||
*
|
||||
|
@ -333,33 +337,27 @@ module.exports = React.createClass({
|
|||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
var itemlist = this.refs.itemlist;
|
||||
var tiles = itemlist.children;
|
||||
const tiles = this.refs.itemlist.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
||||
// Subtract clientHeights to simulate the events being unpaginated whilst counting
|
||||
// the events to be unpaginated.
|
||||
if (backwards) {
|
||||
// Iterate forwards from start of tiles, subtracting event tile height
|
||||
let i = 0;
|
||||
while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i++;
|
||||
// Subtract heights of tiles to simulate the tiles being unpaginated until the
|
||||
// excess height is less than the height of the next tile to subtract. This
|
||||
// prevents excessHeight becoming negative, which could lead to future
|
||||
// pagination.
|
||||
//
|
||||
// If backwards is true, we unpaginate (remove) tiles from the back (top).
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||
// Subtract height of tile as if it were unpaginated
|
||||
excessHeight -= tile.clientHeight;
|
||||
// The tile may not have a scroll token, so guard it
|
||||
if (tile.dataset.scrollTokens) {
|
||||
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
|
||||
}
|
||||
} else {
|
||||
// Iterate backwards from end of tiles, subtracting event tile height
|
||||
let i = tiles.length - 1;
|
||||
while (i > 0 && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i--;
|
||||
if (tile.clientHeight > excessHeight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,7 +423,8 @@ module.exports = React.createClass({
|
|||
* scroll. false if we are tracking a particular child.
|
||||
*
|
||||
* 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,
|
||||
* the number of pixels the bottom of the tracked child is above the
|
||||
|
@ -489,21 +488,25 @@ module.exports = React.createClass({
|
|||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.PAGE_UP:
|
||||
this.scrollRelative(-1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(-1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this.scrollRelative(1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
break;
|
||||
|
@ -553,8 +556,10 @@ module.exports = React.createClass({
|
|||
var messages = this.refs.itemlist.children;
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var m = messages[i];
|
||||
if (!m.dataset.scrollToken) continue;
|
||||
if (m.dataset.scrollToken == scrollToken) {
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
// There might only be one scroll token
|
||||
if (m.dataset.scrollTokens &&
|
||||
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
|
||||
node = m;
|
||||
break;
|
||||
}
|
||||
|
@ -570,7 +575,7 @@ module.exports = React.createClass({
|
|||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
|
||||
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||
pixelOffset + " (delta: "+scrollDelta+")");
|
||||
|
||||
if(scrollDelta != 0) {
|
||||
|
@ -589,24 +594,34 @@ module.exports = React.createClass({
|
|||
var itemlist = this.refs.itemlist;
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var messages = itemlist.children;
|
||||
let newScrollState = null;
|
||||
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var node = messages[i];
|
||||
if (!node.dataset.scrollToken) continue;
|
||||
if (!node.dataset.scrollTokens) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
return;
|
||||
newScrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
// If the bottom of the panel intersects the ClientRect of node, use this node
|
||||
// as the scrollToken.
|
||||
// If this is false for the entire for-loop, we default to the last node
|
||||
// (which is why newScrollState is set on every iteration).
|
||||
if (boundingRect.top < wrapperRect.bottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
// This is only false if there were no nodes with `node.dataset.scrollTokens` set.
|
||||
if (newScrollState) {
|
||||
this.scrollState = newScrollState;
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
} else {
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
}
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -102,9 +103,6 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
statics: {
|
||||
// a map from room id to read marker event ID
|
||||
roomReadMarkerMap: {},
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
roomReadMarkerTsMap: {},
|
||||
},
|
||||
|
@ -121,10 +119,14 @@ var TimelinePanel = React.createClass({
|
|||
getInitialState: function() {
|
||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||
// but for now we just do it per room for simplicity.
|
||||
let initialReadMarker = null;
|
||||
if (this.props.manageReadMarkers) {
|
||||
var initialReadMarker =
|
||||
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|
||||
|| this._getCurrentReadReceipt();
|
||||
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||
if (readmarker){
|
||||
initialReadMarker = readmarker.getContent().event_id;
|
||||
} else {
|
||||
initialReadMarker = this._getCurrentReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -166,6 +168,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
backPaginating: 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");
|
||||
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.last_rm_sent_event_id = undefined;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
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.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("sync", this.onSync);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
},
|
||||
|
@ -247,14 +255,18 @@ var TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
||||
// this particular event should be the first or last to be unpaginated.
|
||||
let eventId = scrollToken;
|
||||
|
||||
let marker = this.state.events.findIndex(
|
||||
|
@ -412,6 +424,7 @@ var TimelinePanel = React.createClass({
|
|||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||
updatedState.readMarkerVisible = false;
|
||||
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) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
|
@ -460,6 +477,25 @@ var TimelinePanel = React.createClass({
|
|||
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() {
|
||||
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
|
||||
// very possible have logged out within that timeframe, so check
|
||||
// we still have a client.
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
// if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
|
||||
// to avoid having to wait from the remote echo from the homeserver.
|
||||
if (this.isAtEndOfLiveTimeline()) {
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
|
||||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||
// XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
// if no client or client is guest don't send RR
|
||||
if (!cli || cli.isGuest()) return;
|
||||
|
||||
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
||||
var 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
|
||||
// same one at the server repeatedly
|
||||
if (lastReadEventIndex > currentReadUpToEventIndex
|
||||
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
||||
if ((lastReadEventIndex > currentReadUpToEventIndex &&
|
||||
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();
|
||||
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
|
||||
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.
|
||||
// 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[0].getTs()) {
|
||||
return -1;
|
||||
|
@ -707,6 +768,19 @@ var TimelinePanel = React.createClass({
|
|||
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.
|
||||
*
|
||||
|
@ -717,7 +791,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END)
|
||||
{
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
|
@ -810,7 +886,7 @@ var TimelinePanel = React.createClass({
|
|||
// go via the dispatcher so that the URL is updated
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.timelineSet.roomId,
|
||||
room_id: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -945,16 +1021,12 @@ var TimelinePanel = React.createClass({
|
|||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
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
|
||||
// no change to the RM.
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
if (eventId === this.state.readMarkerEventId) {
|
||||
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
|
||||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||
|
@ -963,6 +1035,7 @@ var TimelinePanel = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// Do the local echo of the RM
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState({
|
||||
|
@ -1011,11 +1084,17 @@ var TimelinePanel = React.createClass({
|
|||
// of paginating our way through the entire history of the room.
|
||||
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 (
|
||||
<MessagePanel ref="messagePanel"
|
||||
hidden={ this.props.hidden }
|
||||
backPaginating={ this.state.backPaginating }
|
||||
forwardPaginating={ this.state.forwardPaginating }
|
||||
forwardPaginating={ forwardPaginating }
|
||||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
|
|
|
@ -25,12 +25,13 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
dis.register(this.onAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.mounted = false;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -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
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var PlatformPeg = require("../../PlatformPeg");
|
||||
var Modal = require('../../Modal');
|
||||
var dis = require("../../dispatcher");
|
||||
var q = require('q');
|
||||
var package_json = require('../../../package.json');
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var Email = require('../../email');
|
||||
var AddThreepid = require('../../AddThreepid');
|
||||
var SdkConfig = require('../../SdkConfig');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const PlatformPeg = require("../../PlatformPeg");
|
||||
const Modal = require('../../Modal');
|
||||
const dis = require("../../dispatcher");
|
||||
const q = require('q');
|
||||
const packageJson = require('../../../package.json');
|
||||
const UserSettingsStore = require('../../UserSettingsStore');
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
const Email = require('../../email');
|
||||
const AddThreepid = require('../../AddThreepid');
|
||||
const SdkConfig = require('../../SdkConfig');
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
// if this looks like a release, use the 'version' from package.json; else use
|
||||
// the git sha.
|
||||
const REACT_SDK_VERSION =
|
||||
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
|
||||
// the git sha. Prepend version with v, to look like riot-web version
|
||||
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.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).
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
|
@ -43,6 +57,14 @@ const SETTINGS_LABELS = [
|
|||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: 'Hide read receipts',
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: "Don't send typing notifications",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
|
@ -93,7 +115,7 @@ const THEMES = [
|
|||
id: 'theme',
|
||||
label: 'Dark theme',
|
||||
value: 'dark',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
@ -139,6 +161,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._addThreepid = null;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
q().then(() => {
|
||||
|
@ -166,7 +189,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
this._refreshFromServer();
|
||||
|
||||
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
if (!syncedSettings.theme) {
|
||||
syncedSettings.theme = 'light';
|
||||
}
|
||||
|
@ -188,16 +211,16 @@ module.exports = React.createClass({
|
|||
middleOpacity: 1.0,
|
||||
});
|
||||
dis.unregister(this.dispatcherRef);
|
||||
let cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
||||
}
|
||||
},
|
||||
|
||||
_refreshFromServer: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
q.all([
|
||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
|
||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
|
||||
]).done(function(resps) {
|
||||
self.setState({
|
||||
avatarUrl: resps[0].avatar_url,
|
||||
|
@ -205,10 +228,11 @@ module.exports = React.createClass({
|
|||
phase: "UserSettings.DISPLAY",
|
||||
});
|
||||
}, 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, {
|
||||
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) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guests can't set avatars. Please register.",
|
||||
|
@ -235,8 +259,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
var self = this;
|
||||
var changeAvatar = this.refs.changeAvatar;
|
||||
const self = this;
|
||||
const changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
|
@ -245,27 +269,34 @@ module.exports = React.createClass({
|
|||
// dunno if the avatar changed, re-check it.
|
||||
self._refreshFromServer();
|
||||
}, function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
console.error("Failed to set avatar: " + err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
title: "Failed to set avatar",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Sign out?",
|
||||
description:
|
||||
<div>
|
||||
For security, logging out will delete any end-to-end encryption keys from this browser,
|
||||
making previous encrypted chat history unreadable if you log back in.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
|
||||
but for now be warned.
|
||||
For security, logging out will delete any end-to-end encryption keys from this browser.
|
||||
|
||||
If you want to be able to decrypt your conversation history from future Riot sessions,
|
||||
please export your room keys for safe-keeping.
|
||||
</div>,
|
||||
button: "Sign out",
|
||||
extraButtons: [
|
||||
<button key="export" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
|
@ -278,33 +309,33 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPasswordChangeError: function(err) {
|
||||
var errMsg = err.error || "";
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
errMsg = "Failed to change password. Is your password correct?";
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
} else if (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, {
|
||||
title: "Error",
|
||||
description: errMsg
|
||||
description: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordChanged: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Success",
|
||||
description: `Your password was successfully changed. You will not
|
||||
receive push notifications on other devices until you
|
||||
log back in to them.`
|
||||
log back in to them.`,
|
||||
});
|
||||
},
|
||||
|
||||
onUpgradeClicked: function() {
|
||||
dis.dispatch({
|
||||
action: "start_upgrade_registration"
|
||||
action: "start_upgrade_registration",
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -312,23 +343,27 @@ module.exports = React.createClass({
|
|||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
},
|
||||
|
||||
onAddThreepidClicked: function(value, shouldSubmit) {
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
this._addEmail();
|
||||
},
|
||||
|
||||
var email_address = this.refs.add_threepid_input.value;
|
||||
if (!Email.looksValid(email_address)) {
|
||||
_addEmail: function() {
|
||||
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, {
|
||||
title: "Invalid Email Address",
|
||||
description: "This doesn't appear to be a valid email address",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.add_threepid = new AddThreepid();
|
||||
this._addThreepid = new AddThreepid();
|
||||
// we always bind emails when registering, so let's do the
|
||||
// same here.
|
||||
this.add_threepid.addEmailAddress(email_address, true).done(() => {
|
||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Verification Pending",
|
||||
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||
|
@ -337,12 +372,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
}, (err) => {
|
||||
this.setState({email_add_pending: false});
|
||||
console.error("Unable to add email address " + emailAddress + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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});
|
||||
},
|
||||
|
||||
|
@ -361,9 +397,10 @@ module.exports = React.createClass({
|
|||
return this._refreshFromServer();
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to remove contact information",
|
||||
description: err.toString(),
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
|
@ -380,8 +417,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
verifyEmailAddress: function() {
|
||||
this.add_threepid.checkEmailLinkClicked().done(() => {
|
||||
this.add_threepid = undefined;
|
||||
this._addThreepid.checkEmailLinkClicked().done(() => {
|
||||
this._addThreepid = null;
|
||||
this.setState({
|
||||
phase: "UserSettings.LOADING",
|
||||
});
|
||||
|
@ -389,9 +426,9 @@ module.exports = React.createClass({
|
|||
this.setState({email_add_pending: false});
|
||||
}, (err) => {
|
||||
this.setState({email_add_pending: false});
|
||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var message = "Unable to verify email address. ";
|
||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
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.";
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Verification Pending",
|
||||
|
@ -400,10 +437,11 @@ module.exports = React.createClass({
|
|||
onFinished: this.onEmailDialogFinished,
|
||||
});
|
||||
} else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify email address: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to verify email address",
|
||||
description: err.toString(),
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -423,10 +461,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onClearCacheClicked: function() {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().store.deleteAllData().done(() => {
|
||||
// forceReload=false since we don't really need new HTML/JS files
|
||||
// we just need to restart the JS runtime.
|
||||
window.location.reload(false);
|
||||
PlatformPeg.get().reload();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -438,17 +477,17 @@ module.exports = React.createClass({
|
|||
|
||||
_onRejectAllInvitesClicked: function(rooms, ev) {
|
||||
this.setState({
|
||||
rejectingInvites: true
|
||||
rejectingInvites: true,
|
||||
});
|
||||
// reject the invites
|
||||
let promises = rooms.map((room) => {
|
||||
const promises = rooms.map((room) => {
|
||||
return MatrixClientPeg.get().leave(room.roomId);
|
||||
});
|
||||
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||
// after trying to reject all the invites.
|
||||
q.allSettled(promises).then(() => {
|
||||
this.setState({
|
||||
rejectingInvites: false
|
||||
rejectingInvites: false,
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
@ -461,7 +500,7 @@ module.exports = React.createClass({
|
|||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -473,7 +512,7 @@ module.exports = React.createClass({
|
|||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -499,8 +538,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderUserInterfaceSettings: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>User Interface</h3>
|
||||
|
@ -527,7 +564,7 @@ module.exports = React.createClass({
|
|||
<input id="urlPreviewsDisabled"
|
||||
type="checkbox"
|
||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
||||
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
||||
/>
|
||||
<label htmlFor="urlPreviewsDisabled">
|
||||
Disable inline URL previews by default
|
||||
|
@ -540,7 +577,7 @@ module.exports = React.createClass({
|
|||
<input id={ setting.id }
|
||||
type="checkbox"
|
||||
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 }>
|
||||
{ setting.label }
|
||||
|
@ -555,7 +592,7 @@ module.exports = React.createClass({
|
|||
name={ setting.id }
|
||||
value={ setting.value }
|
||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||
onChange={ e => {
|
||||
onChange={ (e) => {
|
||||
if (e.target.checked) {
|
||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||
}
|
||||
|
@ -617,8 +654,8 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
defaultChecked={ this._localSettings[setting.id] }
|
||||
onChange={
|
||||
e => {
|
||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
|
||||
(e) => {
|
||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
|
||||
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
|
||||
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
|
||||
}
|
||||
|
@ -632,7 +669,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
|
@ -643,7 +680,7 @@ module.exports = React.createClass({
|
|||
|
||||
_renderBugReport: function() {
|
||||
if (!SdkConfig.get().bug_report_endpoint_url) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
@ -662,17 +699,17 @@ module.exports = React.createClass({
|
|||
// default to enabled if undefined
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
e.target.checked = false;
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guests can't use labs features. Please register.",
|
||||
|
@ -724,14 +761,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderBulkOptions: function() {
|
||||
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||
return r.hasMembershipState(this._me, "invite");
|
||||
});
|
||||
if (invitedRooms.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let Spinner = sdk.getComponent("elements.Spinner");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let reject = <Spinner />;
|
||||
if (!this.state.rejectingInvites) {
|
||||
|
@ -753,13 +790,33 @@ module.exports = React.createClass({
|
|||
</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) {
|
||||
if (medium == 'msisdn') return 'Phone';
|
||||
if (medium === 'msisdn') return 'Phone';
|
||||
return medium[0].toUpperCase() + medium.slice(1);
|
||||
},
|
||||
|
||||
presentableTextForThreepid: function(threepid) {
|
||||
if (threepid.medium === 'msisdn') {
|
||||
return '+' + threepid.address;
|
||||
} else {
|
||||
return threepid.address;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
switch (this.state.phase) {
|
||||
case "UserSettings.LOADING":
|
||||
return (
|
||||
|
@ -771,18 +828,18 @@ module.exports = React.createClass({
|
|||
throw new Error("Unknown state.phase => " + this.state.phase);
|
||||
}
|
||||
// can only get here if phase is UserSettings.DISPLAY
|
||||
var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var Notifications = sdk.getComponent("settings.Notifications");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const Notifications = sdk.getComponent("settings.Notifications");
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
var avatarUrl = (
|
||||
const avatarUrl = (
|
||||
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;
|
||||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
|
@ -790,7 +847,9 @@ module.exports = React.createClass({
|
|||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input type="text" key={val.address} id={id} value={val.address} disabled />
|
||||
<input type="text" key={val.address} id={id}
|
||||
value={this.presentableTextForThreepid(val)} disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||
|
@ -798,32 +857,37 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
});
|
||||
var addThreepidSection;
|
||||
let addEmailSection;
|
||||
if (this.state.email_add_pending) {
|
||||
addThreepidSection = <Loader />;
|
||||
addEmailSection = <Loader key="_email_add_spinner" />;
|
||||
} else if (!MatrixClientPeg.get().isGuest()) {
|
||||
addThreepidSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="new">
|
||||
addEmailSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<EditableText
|
||||
ref="add_threepid_input"
|
||||
ref="add_email_input"
|
||||
className="mx_UserSettings_editable"
|
||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||
placeholder={ "Add email address" }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onAddThreepidClicked } />
|
||||
onValueChanged={ this._onAddEmailEditFinished } />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={this._addEmail} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
threepidsSection.push(addThreepidSection);
|
||||
const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber');
|
||||
const addMsisdnSection = (
|
||||
<AddPhoneNumber key="_addMsisdn" onThreepidAdded={this._refreshFromServer} />
|
||||
);
|
||||
threepidsSection.push(addEmailSection);
|
||||
threepidsSection.push(addMsisdnSection);
|
||||
|
||||
var accountJsx;
|
||||
let accountJsx;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
accountJsx = (
|
||||
|
@ -831,8 +895,7 @@ module.exports = React.createClass({
|
|||
Create an account
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
accountJsx = (
|
||||
<ChangePassword
|
||||
className="mx_UserSettings_accountTable"
|
||||
|
@ -844,9 +907,9 @@ module.exports = React.createClass({
|
|||
onFinished={this.onPasswordChanged} />
|
||||
);
|
||||
}
|
||||
var notification_area;
|
||||
let notificationArea;
|
||||
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
|
||||
notification_area = (<div>
|
||||
notificationArea = (<div>
|
||||
<h3>Notifications</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -855,12 +918,12 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
var olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
const olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
// 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.
|
||||
var olmVersionString = "<not-enabled>";
|
||||
let olmVersionString = "<not-enabled>";
|
||||
if (olmVersion !== undefined) {
|
||||
olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2];
|
||||
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -918,7 +981,7 @@ module.exports = React.createClass({
|
|||
|
||||
{this._renderReferral()}
|
||||
|
||||
{notification_area}
|
||||
{notificationArea}
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
|
@ -933,6 +996,12 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_advanced">
|
||||
Logged in as {this._me}
|
||||
</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">
|
||||
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
|
||||
</div>
|
||||
|
@ -940,8 +1009,14 @@ module.exports = React.createClass({
|
|||
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
||||
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
|
||||
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
|
||||
? 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/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -953,5 +1028,5 @@ module.exports = React.createClass({
|
|||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -93,11 +93,17 @@ module.exports = React.createClass({
|
|||
description:
|
||||
<div>
|
||||
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
||||
but for now be warned.
|
||||
making encrypted chat history unreadable, unless you first export your room keys
|
||||
and re-import them afterwards.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
|
@ -110,6 +116,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
this.setState({
|
||||
[stateKey]: ev.target.value
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,13 +17,14 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../../index');
|
||||
var Login = require("../../../Login");
|
||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||
var CasLogin = require("../../views/login/CasLogin");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import url from 'url';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
|
@ -52,20 +54,21 @@ module.exports = React.createClass({
|
|||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
|
||||
initialErrorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
errorText: this.props.initialErrorText,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
|
||||
// used for preserving username when changing homeserver
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
currentFlow: "m.login.password",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -73,20 +76,21 @@ module.exports = React.createClass({
|
|||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, password) {
|
||||
var self = this;
|
||||
self.setState({
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
||||
self.props.onLoggedIn(data);
|
||||
}, function(error) {
|
||||
self._setStateFromError(error, true);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
this._loginLogic.loginViaPassword(
|
||||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
this._setStateFromError(error, true);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
busy: false
|
||||
});
|
||||
}).done();
|
||||
|
@ -119,23 +123,36 @@ module.exports = React.createClass({
|
|||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
var self = this;
|
||||
onPhoneCountryChanged: function(phoneCountry) {
|
||||
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({
|
||||
enteredHomeserverUrl: newHsUrl,
|
||||
errorText: null, // reset err messages
|
||||
}, function() {
|
||||
self._initLoginLogic(newHsUrl);
|
||||
phoneNumber: phoneNumber,
|
||||
errorText: null,
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
onServerConfigChange: function(config) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl,
|
||||
let newState = {
|
||||
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;
|
||||
|
||||
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({
|
||||
enteredHomeserverUrl: hsUrl,
|
||||
enteredIdentityServerUrl: isUrl,
|
||||
busy: true,
|
||||
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() {
|
||||
|
@ -221,16 +241,29 @@ module.exports = React.createClass({
|
|||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
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 (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsDomain={hsDomain}
|
||||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
|
@ -248,10 +281,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
|
@ -277,15 +311,14 @@ module.exports = React.createClass({
|
|||
<h2>Sign in
|
||||
{ loader }
|
||||
</h2>
|
||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
|
|
|
@ -123,18 +123,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.setState({
|
||||
hsUrl: newHsUrl,
|
||||
onServerConfigChange: function(config) {
|
||||
let newState = {};
|
||||
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() {
|
||||
|
@ -155,10 +154,21 @@ module.exports = React.createClass({
|
|||
|
||||
_onUIAuthFinished: function(success, response, extra) {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdn_available = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
}
|
||||
if (!msisdn_available) {
|
||||
msg = "This server does not support authentication with a phone number";
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
doingUIAuth: false,
|
||||
errorText: response.message || response.toString(),
|
||||
errorText: msg,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -185,7 +195,6 @@ module.exports = React.createClass({
|
|||
const teamToken = data.team_token;
|
||||
// Store for use /w welcome pages
|
||||
window.localStorage.setItem('mx_team_token', teamToken);
|
||||
this.props.onTeamMemberRegistered(teamToken);
|
||||
|
||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
|
@ -262,6 +271,9 @@ module.exports = React.createClass({
|
|||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = "This doesn't look like a valid email address";
|
||||
break;
|
||||
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||
errMsg = "This doesn't look like a valid phone number";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||
break;
|
||||
|
@ -296,15 +308,20 @@ module.exports = React.createClass({
|
|||
guestAccessToken = null;
|
||||
}
|
||||
|
||||
// Only send the bind params if we're sending username / pw params
|
||||
// (Since we need to send no params at all to use the ones saved in the
|
||||
// session).
|
||||
const bindThreepids = this.state.formVals.password ? {
|
||||
email: true,
|
||||
msisdn: true,
|
||||
} : {};
|
||||
|
||||
return this._matrixClient.register(
|
||||
this.state.formVals.username,
|
||||
this.state.formVals.password,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
// Only send the bind_email param if we're sending username / pw params
|
||||
// (Since we need to send no params at all to use the ones saved in the
|
||||
// session).
|
||||
Boolean(this.state.formVals.username) || undefined,
|
||||
bindThreepids,
|
||||
guestAccessToken,
|
||||
);
|
||||
},
|
||||
|
@ -355,6 +372,8 @@ module.exports = React.createClass({
|
|||
<RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
teamsConfig={this.state.teamsConfig}
|
||||
guestUsername={guestUsername}
|
||||
|
@ -370,8 +389,7 @@ module.exports = React.createClass({
|
|||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,9 @@ module.exports = React.createClass({
|
|||
ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
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
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props),
|
||||
|
@ -74,7 +76,9 @@ module.exports = React.createClass({
|
|||
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
props.width, props.height, props.resizeMethod,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
);
|
||||
},
|
||||
|
@ -103,14 +107,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
return theOtherGuy.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
props.width, props.height, props.resizeMethod,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -46,7 +47,19 @@ export default React.createClass({
|
|||
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) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -65,15 +78,14 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<div onKeyUp={this._onKeyUp} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<img
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
alt="Cancel" title="Cancel"
|
||||
/>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
{ this.props.title }
|
||||
|
|
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;
|
||||
|
||||
/*
|
||||
* Escapes a string so it can be used in a RegExp
|
||||
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
|
||||
* From http://stackoverflow.com/a/6969486
|
||||
*/
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "ChatInviteDialog",
|
||||
propTypes: {
|
||||
|
@ -111,18 +102,27 @@ module.exports = React.createClass({
|
|||
if (inviteList === null) return;
|
||||
}
|
||||
|
||||
const addrTexts = inviteList.map(addr => addr.address);
|
||||
if (inviteList.length > 0) {
|
||||
if (this._isDmChat(inviteList)) {
|
||||
if (this._isDmChat(addrTexts)) {
|
||||
const userId = inviteList[0].address;
|
||||
// Direct Message chat
|
||||
var room = this._getDirectMessageRoom(inviteList[0]);
|
||||
if (room) {
|
||||
// A Direct Message room already exists for this user and you
|
||||
// so go straight to that room
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
const rooms = this._getDirectMessageRooms(userId);
|
||||
if (rooms.length > 0) {
|
||||
// A Direct Message room already exists for this user, so select a
|
||||
// room from a list that is similar to the one in MemberInfo panel
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
"views.dialogs.ChatCreateOrReuseDialog"
|
||||
);
|
||||
Modal.createDialog(ChatCreateOrReuseDialog, {
|
||||
userId: userId,
|
||||
onFinished: (success) => {
|
||||
if (success) {
|
||||
this.props.onFinished(true, inviteList[0]);
|
||||
}
|
||||
// else show this ChatInviteDialog again
|
||||
}
|
||||
});
|
||||
this.props.onFinished(true, inviteList[0]);
|
||||
} else {
|
||||
this._startChat(inviteList);
|
||||
}
|
||||
|
@ -211,20 +211,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
// If the query isn't a user we know about, but is a
|
||||
// valid address, add an entry for that
|
||||
if (queryList.length == 0) {
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList[0] = {
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
};
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -267,22 +266,20 @@ module.exports = React.createClass({
|
|||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_getDirectMessageRoom: function(addr) {
|
||||
_getDirectMessageRooms: function(addr) {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
var dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
if (dmRooms.length > 0) {
|
||||
// Cycle through all the DM rooms and find the first non forgotten or parted room
|
||||
for (let i = 0; i < dmRooms.length; i++) {
|
||||
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
|
||||
if (room) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me.membership == 'join') {
|
||||
return room;
|
||||
}
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
const rooms = [];
|
||||
dmRooms.forEach(dmRoom => {
|
||||
let room = MatrixClientPeg.get().getRoom(dmRoom);
|
||||
if (room) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me.membership == 'join') {
|
||||
rooms.push(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return rooms;
|
||||
},
|
||||
|
||||
_startChat: function(addrs) {
|
||||
|
@ -311,8 +308,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite",
|
||||
description: err.toString()
|
||||
title: "Failed to invite",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -324,8 +321,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite user",
|
||||
description: err.toString()
|
||||
title: "Failed to invite user",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -345,8 +342,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to invite",
|
||||
description: err.toString()
|
||||
title: "Failed to invite",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -381,8 +378,11 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
_isDmChat: function(addrs) {
|
||||
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
_isDmChat: function(addrTexts) {
|
||||
if (addrTexts.length === 1 &&
|
||||
getAddressType(addrTexts[0]) === "mx" &&
|
||||
!this.props.roomId
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
|
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_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={72} height={72} />
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Lifecycle from '../../../Lifecycle';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Velocity from 'velocity-vector';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
|
|
|
@ -50,6 +50,12 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
|
@ -59,7 +65,7 @@ export default React.createClass({
|
|||
{this.props.description}
|
||||
</div>
|
||||
<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}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -21,10 +21,8 @@ export default React.createClass({
|
|||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
]),
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
@ -34,6 +32,7 @@ export default React.createClass({
|
|||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
extraButtons: null,
|
||||
button: "OK",
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
|
@ -48,6 +47,12 @@ export default React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
|
@ -64,9 +69,10 @@ export default React.createClass({
|
|||
{this.props.description}
|
||||
</div>
|
||||
<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}
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -149,7 +149,7 @@ export default React.createClass({
|
|||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
This room contains devices that you haven't seen before.
|
||||
"{this.props.room.name}" contains devices that you haven't seen before.
|
||||
</h4>
|
||||
{ warning }
|
||||
Unknown devices:
|
||||
|
|
|
@ -27,11 +27,13 @@ import React from 'react';
|
|||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, ...restProps} = props;
|
||||
restProps.onClick = onClick;
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
||||
restProps.onKeyUp = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
"mx_AccessibleButton";
|
||||
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 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.
|
||||
|
@ -138,7 +139,7 @@ export default React.createClass({
|
|||
onClick={this.onClick.bind(this, i)}
|
||||
onMouseEnter={this.onMouseEnter.bind(this, i)}
|
||||
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; }}
|
||||
>
|
||||
<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) {
|
||||
if (ev.key == 'Enter') {
|
||||
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||
if (this.props.onJoinClick) {
|
||||
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",
|
||||
"unbanned": beConjugated + " unbanned",
|
||||
"kicked": beConjugated + " kicked",
|
||||
"changed_name": "changed name",
|
||||
"changed_avatar": "changed avatar",
|
||||
};
|
||||
|
||||
if (Object.keys(map).includes(t)) {
|
||||
|
@ -289,7 +291,24 @@ module.exports = React.createClass({
|
|||
switch (e.mxEvent.getContent().membership) {
|
||||
case 'invite': return 'invited';
|
||||
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':
|
||||
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
|
@ -350,6 +369,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const eventsToRender = this.props.events;
|
||||
const eventIds = eventsToRender.map(e => e.getId()).join(',');
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
|
@ -360,7 +380,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (fewEvents) {
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{expandedEvents}
|
||||
</div>
|
||||
);
|
||||
|
@ -418,7 +438,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{toggleButton}
|
||||
{summaryContainer}
|
||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||
|
|
|
@ -16,17 +16,12 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var roles = {
|
||||
0: 'User',
|
||||
50: 'Moderator',
|
||||
100: 'Admin',
|
||||
};
|
||||
import React from 'react';
|
||||
import * as Roles from '../../../Roles';
|
||||
|
||||
var reverseRoles = {};
|
||||
Object.keys(roles).forEach(function(key) {
|
||||
reverseRoles[roles[key]] = key;
|
||||
Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -49,7 +44,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (roles[this.props.value] === undefined),
|
||||
custom: (Roles.LEVEL_ROLE_MAP[this.props.value] === undefined),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -99,22 +94,34 @@ module.exports = React.createClass({
|
|||
selectValue = "Custom";
|
||||
}
|
||||
else {
|
||||
selectValue = roles[this.props.value] || "Custom";
|
||||
selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
}
|
||||
var select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
}
|
||||
else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
return {
|
||||
value: Roles.LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
}
|
||||
});
|
||||
options.push({ value: "Custom", text: "Custom level" });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value}>{op.text}</option>;
|
||||
});
|
||||
|
||||
select =
|
||||
<select ref="select"
|
||||
value={ this.props.controlled ? selectValue : undefined }
|
||||
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
||||
onChange={ this.onSelectChange }>
|
||||
<option value="User">User (0)</option>
|
||||
<option value="Moderator">Moderator (50)</option>
|
||||
<option value="Admin">Admin (100)</option>
|
||||
<option value="Custom">Custom level</option>
|
||||
{ options }
|
||||
</select>;
|
||||
}
|
||||
|
||||
|
|
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 url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
@ -158,6 +160,7 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
stageParams: React.PropTypes.object.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
busy: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
|
@ -168,6 +171,11 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||
var sitePublicKey = this.props.stageParams.public_key;
|
||||
return (
|
||||
|
@ -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({
|
||||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
|
@ -313,6 +452,7 @@ const AuthEntryComponents = [
|
|||
PasswordAuthEntry,
|
||||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,66 +18,164 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||
initialUsername: React.PropTypes.string,
|
||||
initialPassword: React.PropTypes.string,
|
||||
onUsernameChanged: React.PropTypes.func,
|
||||
onPasswordChanged: React.PropTypes.func,
|
||||
loginIncorrect: React.PropTypes.bool,
|
||||
},
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
hsDomain: "",
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: this.props.initialUsername,
|
||||
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;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onSubmitForm: function(ev) {
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onSubmit(this.state.username, this.state.password);
|
||||
},
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
||||
onUsernameChanged: function(ev) {
|
||||
onUsernameChanged(ev) {
|
||||
this.setState({username: 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.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;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
|
@ -92,14 +191,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
error: this.props.loginIncorrect,
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field" type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
value={this.state.username} onChange={this.onUsernameChanged}
|
||||
placeholder="Email or user name" autoFocus />
|
||||
<br />
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">I want to sign in with my</label>
|
||||
<Dropdown
|
||||
className="mx_Login_type_dropdown"
|
||||
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"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
|
@ -111,4 +221,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
</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 sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
const FIELD_USERNAME = 'field_username';
|
||||
const FIELD_PASSWORD = 'field_password';
|
||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
|
@ -35,6 +38,8 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail: React.PropTypes.string,
|
||||
defaultPhoneCountry: React.PropTypes.string,
|
||||
defaultPhoneNumber: React.PropTypes.string,
|
||||
defaultUsername: React.PropTypes.string,
|
||||
defaultPassword: React.PropTypes.string,
|
||||
teamsConfig: React.PropTypes.shape({
|
||||
|
@ -71,6 +76,8 @@ module.exports = React.createClass({
|
|||
return {
|
||||
fieldValid: {},
|
||||
selectedTeam: null,
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -85,6 +92,7 @@ module.exports = React.createClass({
|
|||
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||
this.validateField(FIELD_PASSWORD);
|
||||
this.validateField(FIELD_USERNAME);
|
||||
this.validateField(FIELD_PHONE_NUMBER);
|
||||
this.validateField(FIELD_EMAIL);
|
||||
|
||||
var self = this;
|
||||
|
@ -118,6 +126,8 @@ module.exports = React.createClass({
|
|||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
password: this.refs.password.value.trim(),
|
||||
email: email,
|
||||
phoneCountry: this.state.phoneCountry,
|
||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -174,6 +184,11 @@ module.exports = React.createClass({
|
|||
const emailValid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber.value;
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
||||
|
@ -233,6 +248,8 @@ module.exports = React.createClass({
|
|||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
return this.refs.email;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
return this.refs.phoneNumber;
|
||||
case FIELD_USERNAME:
|
||||
return this.refs.username;
|
||||
case FIELD_PASSWORD:
|
||||
|
@ -251,6 +268,13 @@ module.exports = React.createClass({
|
|||
return cls;
|
||||
},
|
||||
|
||||
_onPhoneCountryChange(newVal) {
|
||||
this.setState({
|
||||
phoneCountry: newVal.iso2,
|
||||
phonePrefix: newVal.prefix,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
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 = (
|
||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||
);
|
||||
|
@ -300,6 +349,7 @@ module.exports = React.createClass({
|
|||
<form onSubmit={this.onSubmit}>
|
||||
{emailSection}
|
||||
{belowEmailSection}
|
||||
{phoneSection}
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
|
|
|
@ -27,8 +27,7 @@ module.exports = React.createClass({
|
|||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
onHsUrlChanged: React.PropTypes.func,
|
||||
onIsUrlChanged: React.PropTypes.func,
|
||||
onServerConfigChange: React.PropTypes.func,
|
||||
|
||||
// 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.
|
||||
|
@ -50,8 +49,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHsUrlChanged: function() {},
|
||||
onIsUrlChanged: function() {},
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
withToggleButton: false,
|
||||
|
@ -75,7 +73,10 @@ module.exports = React.createClass({
|
|||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
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() {
|
||||
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
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
|
||||
});
|
||||
if (!visible) {
|
||||
this.props.onHsUrlChanged(this.props.defaultHsUrl);
|
||||
this.props.onIsUrlChanged(this.props.defaultIsUrl);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.props.defaultHsUrl,
|
||||
isUrl : this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.onHsUrlChanged(this.state.hs_url);
|
||||
this.props.onIsUrlChanged(this.state.is_url);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -346,7 +346,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<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 }
|
||||
</a>
|
||||
<div className="mx_MImageBody_size">
|
||||
|
@ -360,7 +360,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<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"/>
|
||||
Download {text}
|
||||
</a>
|
||||
|
|
|
@ -56,6 +56,7 @@ module.exports = React.createClass({
|
|||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: content.body && content.body.length > 0 ? content.body : 'Attachment',
|
||||
mxEvent: this.props.mxEvent,
|
||||
};
|
||||
|
||||
|
|
|
@ -16,17 +16,18 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var highlight = require('highlight.js');
|
||||
var HtmlUtils = require('../../../HtmlUtils');
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
var sdk = require('../../../index');
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var Modal = require("../../../Modal");
|
||||
var SdkConfig = require('../../../SdkConfig');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -131,7 +132,8 @@ module.exports = React.createClass({
|
|||
links.push(node);
|
||||
}
|
||||
}
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE") {
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
node.tagName === "BLOCKQUOTE") {
|
||||
continue;
|
||||
}
|
||||
else if (node.children && node.children.length) {
|
||||
|
@ -187,6 +189,15 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onEmoteSenderClick: function(event) {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
dis.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: name.replace(' (IRC)', ''),
|
||||
});
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
var self = this;
|
||||
return {
|
||||
|
@ -273,7 +284,15 @@ module.exports = React.createClass({
|
|||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
return (
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
* <EmojiText>{name}</EmojiText> { body }
|
||||
*
|
||||
<EmojiText
|
||||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{name}
|
||||
</EmojiText>
|
||||
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -22,10 +22,10 @@ module.exports = React.createClass({
|
|||
displayName: 'UnknownBody',
|
||||
|
||||
render: function() {
|
||||
var content = this.props.mxEvent.getContent();
|
||||
const text = this.props.mxEvent.getContent().body;
|
||||
return (
|
||||
<span className="mx_UnknownBody">
|
||||
{content.body}
|
||||
<span className="mx_UnknownBody" title="Redacted or unknown message type">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -25,18 +25,10 @@ var TextForEvent = require('../../../TextForEvent');
|
|||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var dispatcher = require("../../../dispatcher");
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
var ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
var bounce = false;
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
var eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.room.member' : 'messages.TextualEvent',
|
||||
|
@ -48,6 +40,7 @@ var eventTileTypes = {
|
|||
'm.room.third_party_invite' : 'messages.TextualEvent',
|
||||
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||
'm.room.encryption' : 'messages.TextualEvent',
|
||||
'm.room.power_levels' : 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
var MAX_READ_AVATARS = 5;
|
||||
|
@ -73,6 +66,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
|
||||
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
|
||||
* references the same this.props.mxEvent.
|
||||
*/
|
||||
isRedacted: React.PropTypes.bool,
|
||||
|
||||
/* true if this is a continuation of the previous event (which has the
|
||||
* effect of not showing another avatar/displayname
|
||||
*/
|
||||
|
@ -285,9 +284,16 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
getReadAvatars: function() {
|
||||
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
var avatars = [];
|
||||
var left = 0;
|
||||
|
||||
// return early if there are no read receipts
|
||||
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.
|
||||
// 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) {
|
||||
hidden = false;
|
||||
}
|
||||
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
||||
// we could optimise this to reduce the dom size.
|
||||
|
||||
// If hidden, set offset equal to the offset of the final visible avatar or
|
||||
// else set it proportional to index
|
||||
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
|
||||
|
||||
var userId = receipt.roomMember.userId;
|
||||
var readReceiptInfo;
|
||||
|
@ -318,11 +330,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
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)
|
||||
avatars.unshift(
|
||||
|
@ -343,7 +350,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
if (remainder > 0) {
|
||||
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
style={{ right: -(left - 15) }}>{ remainder }+
|
||||
style={{ right: -(left - receiptOffset) }}>{ remainder }+
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +363,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
|
||||
onSenderProfileClick: function(event) {
|
||||
var mxEvent = this.props.mxEvent;
|
||||
dispatcher.dispatch({
|
||||
dis.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
||||
});
|
||||
|
@ -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() {
|
||||
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
|
@ -383,8 +401,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
var msgtype = content.msgtype;
|
||||
var eventType = this.props.mxEvent.getType();
|
||||
|
||||
// Info messages are basically information about commands processed on a
|
||||
// room, or emote messages
|
||||
// Info messages are basically information about commands processed on a room
|
||||
var isInfoMessage = (eventType !== 'm.room.message');
|
||||
|
||||
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 isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
|
||||
var classes = classNames({
|
||||
mx_EventTile: true,
|
||||
|
@ -411,9 +429,14 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
menu: this.state.menu,
|
||||
mx_EventTile_verified: this.state.verified == true,
|
||||
mx_EventTile_unverified: this.state.verified == false,
|
||||
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
|
||||
mx_EventTile_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();
|
||||
|
||||
|
@ -486,6 +509,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
else if (e2eEnabled) {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
||||
}
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
if (this.props.tileShape === "notif") {
|
||||
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
@ -493,15 +518,15 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={ permalink }>
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={ permalink }>
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line" >
|
||||
|
@ -527,10 +552,14 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
<a className="mx_EventTile_senderDetailsLink" href={ permalink }>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={ permalink }
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -545,8 +574,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={ permalink }>
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
<a href={ permalink } onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ e2e }
|
||||
<EventTileType ref="tile"
|
||||
|
@ -564,7 +593,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}));
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
if (e.isRedacted()) return false;
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
||||
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
|
|
|
@ -218,11 +218,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
onKick: function() {
|
||||
const membership = this.props.member.membership;
|
||||
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
member: this.props.member,
|
||||
action: 'Kick',
|
||||
askReason: true,
|
||||
action: kickLabel,
|
||||
askReason: membership == "join",
|
||||
danger: true,
|
||||
onFinished: (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
@ -237,9 +239,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Kick success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Kick error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
title: "Failed to kick",
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -278,9 +281,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Ban success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Ban error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message,
|
||||
title: "Error",
|
||||
description: "Failed to ban user",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -327,9 +331,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
console.error("Mute error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to mute user",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -375,9 +380,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
description: "This action cannot be performed by a guest user. Please register to be able to do this."
|
||||
});
|
||||
} else {
|
||||
console.error("Toggle moderator error:" + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Moderator toggle error",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to toggle moderator status",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -395,9 +401,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to change power level",
|
||||
description: err.message
|
||||
title: "Error",
|
||||
description: "Failed to change power level",
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -553,6 +560,13 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
},
|
||||
|
||||
_renderDevices: function() {
|
||||
if (!this._enableDevices) {
|
||||
return null;
|
||||
|
@ -613,6 +627,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
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.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this.onPageUnload = this.onPageUnload.bind(this);
|
||||
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
|
@ -50,7 +51,7 @@ export default class MessageComposer extends React.Component {
|
|||
inputState: {
|
||||
style: [],
|
||||
blockType: null,
|
||||
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
|
||||
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
|
||||
wordCount: 0,
|
||||
},
|
||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||
|
@ -64,12 +65,21 @@ export default class MessageComposer extends React.Component {
|
|||
// marked as encrypted.
|
||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||
MatrixClientPeg.get().on("event", this.onEvent);
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||
}
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
}
|
||||
|
||||
onPageUnload(event) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.sentHistory.saveLastTextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
onEvent(event) {
|
||||
|
@ -91,8 +101,9 @@ export default class MessageComposer extends React.Component {
|
|||
this.refs.uploadInput.click();
|
||||
}
|
||||
|
||||
onUploadFileSelected(ev) {
|
||||
let files = ev.target.files;
|
||||
onUploadFileSelected(files, isPasted) {
|
||||
if (!isPasted)
|
||||
files = files.target.files;
|
||||
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
@ -100,7 +111,7 @@ export default class MessageComposer extends React.Component {
|
|||
let fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'}
|
||||
</li>);
|
||||
}
|
||||
|
||||
|
@ -171,7 +182,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onUpArrow() {
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
}
|
||||
|
||||
onDownArrow() {
|
||||
|
@ -299,6 +310,7 @@ export default class MessageComposer extends React.Component {
|
|||
tryComplete={this._tryComplete}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onUploadFileSelected={this.onUploadFileSelected}
|
||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
|
|
|
@ -96,8 +96,20 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
constructor(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 = {
|
||||
// whether we're in rich text or markdown mode
|
||||
|
@ -261,6 +273,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
sendTyping(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||
|
@ -404,10 +417,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
handleReturn = (ev) => {
|
||||
if(ev.shiftKey) {
|
||||
handlePastedFiles(files) {
|
||||
this.props.onUploadFileSelected(files, true);
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if (ev.shiftKey) {
|
||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||
return true;
|
||||
}
|
||||
|
@ -442,7 +459,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error",
|
||||
description: err.message,
|
||||
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
|
||||
});
|
||||
});
|
||||
} else if (cmd.error) {
|
||||
|
@ -473,9 +490,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendTextFn = this.client.sendTextMessage;
|
||||
|
||||
if (contentText.startsWith('/me')) {
|
||||
contentText = contentText.replace('/me', '');
|
||||
contentText = contentText.replace('/me ', '');
|
||||
// bit of a hack, but the alternative would be quite complicated
|
||||
if (contentHTML) contentHTML = contentHTML.replace('/me', '');
|
||||
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
|
||||
sendHtmlFn = this.client.sendHtmlEmote;
|
||||
sendTextFn = this.client.sendEmoteMessage;
|
||||
}
|
||||
|
@ -686,6 +703,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
handlePastedFiles={this.handlePastedFiles}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
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 MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
|
@ -311,7 +312,7 @@ export default React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
|
|
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
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";
|
||||
// if (this.props.currentlyActive) ago += " (now?)";
|
||||
return (
|
||||
|
|
|
@ -115,9 +115,10 @@ module.exports = React.createClass({
|
|||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set avatar: " + errMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
description: "Failed to set avatar.",
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,15 +22,23 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var RoomListSorter = require("../../../RoomListSorter");
|
||||
var Unread = require('../../../Unread');
|
||||
var dis = require("../../../dispatcher");
|
||||
var sdk = require('../../../index');
|
||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||
var Rooms = require('../../../Rooms');
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
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({
|
||||
displayName: 'RoomList',
|
||||
|
@ -37,13 +46,23 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
currentRoom: React.PropTypes.string,
|
||||
selectedRoom: 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() {
|
||||
return {
|
||||
isLoadingLeftRooms: false,
|
||||
totalRoomCount: null,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
};
|
||||
|
@ -57,12 +76,21 @@ module.exports = React.createClass({
|
|||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("Room.tags", this.onRoomTags);
|
||||
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("accountData", this.onAccountData);
|
||||
|
||||
var s = this.getRoomLists();
|
||||
this.setState(s);
|
||||
// lookup for which lists a given roomId is currently in.
|
||||
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() {
|
||||
|
@ -71,7 +99,22 @@ module.exports = React.createClass({
|
|||
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
|
||||
this._updateStickyHeaders(true);
|
||||
this._repositionIncomingCallBox(undefined, false);
|
||||
|
@ -95,6 +138,26 @@ module.exports = React.createClass({
|
|||
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;
|
||||
}
|
||||
},
|
||||
|
@ -108,7 +171,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
||||
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("accountData", this.onAccountData);
|
||||
}
|
||||
|
@ -117,10 +180,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
|
@ -143,6 +210,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onMouseOver: function(ev) {
|
||||
this._lastMouseOverTs = Date.now();
|
||||
},
|
||||
|
||||
onSubListHeaderClick: function(isHidden, scrollToPosition) {
|
||||
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
|
||||
this._updateStickyHeaders(true, scrollToPosition);
|
||||
|
@ -152,41 +223,98 @@ module.exports = React.createClass({
|
|||
if (toStartOfTimeline) return;
|
||||
if (!room) 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) {
|
||||
// because if we read a notification, it will affect notification count
|
||||
// only bother updating if there's a receipt from us
|
||||
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) {
|
||||
this._delayedRefreshRoomList();
|
||||
constantTimeDispatcher.dispatch(
|
||||
"RoomTile.refresh", room.roomId, {}
|
||||
);
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
this._delayedRefreshRoomList();
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
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) {
|
||||
this._delayedRefreshRoomList();
|
||||
constantTimeDispatcher.dispatch(
|
||||
"RoomTile.refresh", member.roomId, {}
|
||||
);
|
||||
},
|
||||
|
||||
onAccountData: function(ev) {
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
_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),
|
||||
|
||||
refreshRoomList: function() {
|
||||
|
@ -194,26 +322,36 @@ module.exports = React.createClass({
|
|||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||
// );
|
||||
|
||||
// TODO: rather than bluntly regenerating and re-sorting everything
|
||||
// every time we see any kind of room change from the JS SDK
|
||||
// we could do incremental updates on our copy of the state
|
||||
// based on the room which has actually changed. This would stop
|
||||
// us re-rendering all the sublists every time anything changes anywhere
|
||||
// in the state of the client.
|
||||
this.setState(this.getRoomLists());
|
||||
this._lastRefreshRoomListTs = Date.now();
|
||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||
// any changes to it incrementally, updating the appropriate sublists
|
||||
// as needed.
|
||||
// Alternatively we'd do something magical with Immutable.js or similar.
|
||||
const lists = this.getRoomLists();
|
||||
let totalRooms = 0;
|
||||
for (const l of Object.values(lists)) {
|
||||
totalRooms += l.length;
|
||||
}
|
||||
this.setState({
|
||||
lists: this.getRoomLists(),
|
||||
totalRoomCount: totalRooms,
|
||||
});
|
||||
|
||||
// this._lastRefreshRoomListTs = Date.now();
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
var self = this;
|
||||
var s = { lists: {} };
|
||||
const lists = {};
|
||||
|
||||
s.lists["im.vector.fake.invite"] = [];
|
||||
s.lists["m.favourite"] = [];
|
||||
s.lists["im.vector.fake.recent"] = [];
|
||||
s.lists["im.vector.fake.direct"] = [];
|
||||
s.lists["m.lowpriority"] = [];
|
||||
s.lists["im.vector.fake.archived"] = [];
|
||||
lists["im.vector.fake.invite"] = [];
|
||||
lists["m.favourite"] = [];
|
||||
lists["im.vector.fake.recent"] = [];
|
||||
lists["im.vector.fake.direct"] = [];
|
||||
lists["m.lowpriority"] = [];
|
||||
lists["im.vector.fake.archived"] = [];
|
||||
|
||||
this.listsForRoomId = {};
|
||||
var otherTagNames = {};
|
||||
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
|
||||
|
@ -226,8 +364,13 @@ module.exports = React.createClass({
|
|||
// ", target = " + me.events.member.getStateKey() +
|
||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||
|
||||
if (!self.listsForRoomId[room.roomId]) {
|
||||
self.listsForRoomId[room.roomId] = [];
|
||||
}
|
||||
|
||||
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)) {
|
||||
// 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
|
||||
var tagNames = Object.keys(room.tags);
|
||||
|
||||
if (tagNames.length) {
|
||||
for (var i = 0; i < tagNames.length; i++) {
|
||||
var tagName = tagNames[i];
|
||||
s.lists[tagName] = s.lists[tagName] || [];
|
||||
s.lists[tagNames[i]].push(room);
|
||||
lists[tagName] = lists[tagName] || [];
|
||||
lists[tagName].push(room);
|
||||
self.listsForRoomId[room.roomId].push(tagName);
|
||||
otherTagNames[tagName] = 1;
|
||||
}
|
||||
}
|
||||
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
// "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 {
|
||||
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") {
|
||||
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 {
|
||||
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.
|
||||
|
||||
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() {
|
||||
var panel = ReactDOM.findDOMNode(this);
|
||||
if (!panel) return null;
|
||||
|
||||
if (panel.classList.contains('gm-prevented')) {
|
||||
return panel;
|
||||
} else {
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
}
|
||||
// empirically, if we have gm-prevented for some reason, the scroll node
|
||||
// is still the 3rd child (i.e. the view child). This looks to be due
|
||||
// to vdh's improved resize updater logic...?
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
},
|
||||
|
||||
_whenScrolling: function(e) {
|
||||
|
@ -331,10 +455,11 @@ module.exports = React.createClass({
|
|||
var incomingCallBox = document.getElementById("incomingCallBox");
|
||||
if (incomingCallBox && incomingCallBox.parentElement) {
|
||||
var scrollArea = this._getScrollNode();
|
||||
if (!scrollArea) return;
|
||||
// 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
|
||||
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
|
||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||
|
||||
|
@ -354,10 +479,11 @@ module.exports = React.createClass({
|
|||
// properly through React
|
||||
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
|
||||
var scrollArea = this._getScrollNode();
|
||||
if (!scrollArea) return;
|
||||
// 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
|
||||
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
|
||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||
|
||||
|
@ -451,21 +577,74 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
|
||||
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
label="Invites"
|
||||
tagName="im.vector.fake.invite"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
@ -473,51 +652,57 @@ module.exports = React.createClass({
|
|||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
label="Favourites"
|
||||
tagName="m.favourite"
|
||||
verb="favourite"
|
||||
emptyContent={this._getEmptyContent('m.favourite')}
|
||||
editable={ true }
|
||||
order="manual"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
||||
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"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
alwaysShowHeader={ true }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||
label="Rooms"
|
||||
tagName="im.vector.fake.recent"
|
||||
editable={ true }
|
||||
verb="restore"
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
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))$/)) {
|
||||
return <RoomSubList list={ self.state.lists[tagName] }
|
||||
key={ tagName }
|
||||
label={ tagName }
|
||||
tagName={ tagName }
|
||||
verb={ "tag as " + tagName }
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={ true }
|
||||
order="manual"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />;
|
||||
|
@ -528,22 +713,23 @@ module.exports = React.createClass({
|
|||
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||
label="Low priority"
|
||||
tagName="m.lowpriority"
|
||||
verb="demote"
|
||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||
editable={ true }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||
label="Historical"
|
||||
tagName="im.vector.fake.archived"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
alwaysShowHeader={ true }
|
||||
startAsHidden={ true }
|
||||
showSpinner={ self.state.isLoadingLeftRooms }
|
||||
|
|
|
@ -54,9 +54,10 @@ const BannedUser = React.createClass({
|
|||
this.props.member.roomId, this.props.member.userId,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to unban: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to unban",
|
||||
description: err.message,
|
||||
title: "Error",
|
||||
description: "Failed to unban",
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
@ -128,14 +129,17 @@ module.exports = React.createClass({
|
|||
console.error("Failed to get room visibility: " + err);
|
||||
});
|
||||
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
|
@ -489,7 +493,7 @@ module.exports = React.createClass({
|
|||
ev.preventDefault();
|
||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: this.scalarClient.hasCredentials() ?
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||
null,
|
||||
onFinished: ()=>{
|
||||
|
@ -764,36 +768,39 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
var integrationsButton;
|
||||
var 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>
|
||||
);
|
||||
}
|
||||
let integrationsButton;
|
||||
let integrationsError;
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
if (this.scalarClient !== null) {
|
||||
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 }>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var classNames = require('classnames');
|
||||
var dis = require("../../../dispatcher");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
var sdk = require('../../../index');
|
||||
|
@ -28,6 +27,8 @@ var RoomNotifs = require('../../../RoomNotifs');
|
|||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
|
||||
var Unread = require('../../../Unread');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -35,13 +36,12 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
connectDragSource: React.PropTypes.func,
|
||||
connectDropTarget: React.PropTypes.func,
|
||||
onClick: React.PropTypes.func,
|
||||
isDragging: React.PropTypes.bool,
|
||||
selectedRoom: React.PropTypes.string,
|
||||
|
||||
room: React.PropTypes.object.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,
|
||||
incomingCall: React.PropTypes.object,
|
||||
},
|
||||
|
@ -54,11 +54,11 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return({
|
||||
hover : false,
|
||||
badgeHover : false,
|
||||
notificationTagMenu: false,
|
||||
roomTagMenu: false,
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
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() {
|
||||
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() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
||||
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.room.roomId,
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.onRefresh();
|
||||
},
|
||||
|
||||
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() {
|
||||
this.setState( { hover : true });
|
||||
this.badgeOnMouseEnter();
|
||||
|
@ -137,62 +145,32 @@ module.exports = React.createClass({
|
|||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu');
|
||||
var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
var elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = elementRect.right + window.pageXOffset + 3;
|
||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(NotificationStateMenu, {
|
||||
menuWidth: 188,
|
||||
menuHeight: 126,
|
||||
chevronOffset: 45,
|
||||
ContextualMenu.createMenu(RoomTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ notificationTagMenu: false });
|
||||
self.setState({ menuDisplayed: false });
|
||||
self.props.refreshSubList();
|
||||
}
|
||||
});
|
||||
this.setState({ notificationTagMenu: true });
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
onAvatarClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest() && !this.props.collapsed) {
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
var RoomTagMenu = sdk.getComponent('context_menus.RoomTagContextMenu');
|
||||
var elementRect = e.target.getBoundingClientRect();
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = elementRect.right + window.pageXOffset + 3;
|
||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 19;
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(RoomTagMenu, {
|
||||
chevronOffset: 10,
|
||||
// XXX: fix horrid hardcoding
|
||||
menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF",
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ roomTagMenu: false });
|
||||
}
|
||||
});
|
||||
this.setState({ roomTagMenu: true });
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var me = this.props.room.currentState.members[myUserId];
|
||||
|
@ -201,17 +179,17 @@ module.exports = React.createClass({
|
|||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
||||
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
var classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.props.selected,
|
||||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': this.state.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
});
|
||||
|
||||
|
@ -219,14 +197,9 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_avatar': true,
|
||||
});
|
||||
|
||||
var avatarContainerClasses = classNames({
|
||||
'mx_RoomTile_avatar_container': true,
|
||||
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.notificationTagMenu,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
// XXX: We should never display raw room IDs, but sometimes the
|
||||
|
@ -237,7 +210,7 @@ module.exports = React.createClass({
|
|||
var badge;
|
||||
var badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.notificationTagMenu) {
|
||||
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||
badgeContent = "\u00B7\u00B7\u00B7";
|
||||
} else if (badges) {
|
||||
var limitedCount = FormattingUtils.formatCount(notificationCount);
|
||||
|
@ -255,10 +228,10 @@ module.exports = React.createClass({
|
|||
var nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.props.isInvite,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
if (this.props.selected) {
|
||||
if (this.state.selected) {
|
||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||
|
||||
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
||||
|
@ -292,13 +265,12 @@ module.exports = React.createClass({
|
|||
|
||||
let ret = (
|
||||
<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="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
||||
<div className={avatarContainerClasses}>
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{directMessageIndicator}
|
||||
</div>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{directMessageIndicator}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
|
|
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<li data-scroll-token={eventId+"+"+j}>
|
||||
<li data-scroll-tokens={eventId+"+"+j}>
|
||||
{ret}
|
||||
</li>);
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
// cancel button which is shared between room header and simple room header
|
||||
export function CancelButton(props) {
|
||||
|
@ -45,6 +46,9 @@ export default React.createClass({
|
|||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
icon: React.PropTypes.string,
|
||||
},
|
||||
|
||||
onShowRhsClick: function(ev) {
|
||||
|
@ -53,9 +57,17 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
let cancelButton;
|
||||
let icon;
|
||||
if (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;
|
||||
/* // 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_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ showRhsButton }
|
||||
{ cancelButton }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -32,10 +33,10 @@ module.exports = React.createClass({
|
|||
<div className="mx_TopUnreadMessagesBar">
|
||||
<div className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
<img src="img/scrollup.svg" width="24" height="24"
|
||||
<img src="img/scrollto.svg" width="24" height="24"
|
||||
alt="Scroll to unread messages"
|
||||
title="Scroll to unread messages"/>
|
||||
Unread messages. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
|
||||
Jump to first unread message.
|
||||
</div>
|
||||
<img className="mx_TopUnreadMessagesBar_close"
|
||||
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:
|
||||
<div>
|
||||
Changing password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
This will be <a href="https://github.com/vector-im/riot-web/issues/2671">improved shortly</a>,
|
||||
but for now be warned.
|
||||
making encrypted chat history unreadable, unless you first export your room keys
|
||||
and re-import them afterwards.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</button>
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
var authDict = {
|
||||
|
@ -105,6 +111,18 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onClickChange: function() {
|
||||
var old_password = this.refs.old_input.value;
|
||||
var new_password = this.refs.new_input.value;
|
||||
|
|
|
@ -102,9 +102,10 @@ function createRoom(opts) {
|
|||
});
|
||||
return roomId;
|
||||
}, function(err) {
|
||||
console.error("Failed to create room " + roomId + " " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to create room",
|
||||
description: err.toString()
|
||||
description: "Server may be unavailable, overloaded, or you hit a bug.",
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
|
|||
// anyone else really should be using matrix.to.
|
||||
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||
+ escapeRegExp(window.location.host + window.location.pathname) + "|"
|
||||
+ "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/"
|
||||
+ "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/"
|
||||
+ ")(#.*)";
|
||||
|
||||
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.
|
||||
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',
|
||||
backgroundColor: '#fff8dc' }}>
|
||||
{key}
|
||||
|
|
|
@ -68,48 +68,49 @@ describe('InteractiveAuthDialog', function () {
|
|||
onFinished={onFinished}
|
||||
/>, parentDiv);
|
||||
|
||||
// at this point there should be a password box and a submit button
|
||||
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
|
||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||
dlg, "input"
|
||||
);
|
||||
let passwordNode;
|
||||
let submitNode;
|
||||
for (const node of inputNodes) {
|
||||
if (node.type == 'password') {
|
||||
passwordNode = node;
|
||||
} else if (node.type == 'submit') {
|
||||
submitNode = node;
|
||||
// wait for a password box and a submit button
|
||||
test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => {
|
||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||
dlg, "input"
|
||||
);
|
||||
let passwordNode;
|
||||
let submitNode;
|
||||
for (const node of inputNodes) {
|
||||
if (node.type == 'password') {
|
||||
passwordNode = node;
|
||||
} else if (node.type == 'submit') {
|
||||
submitNode = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(passwordNode).toExist();
|
||||
expect(submitNode).toExist();
|
||||
expect(passwordNode).toExist();
|
||||
expect(submitNode).toExist();
|
||||
|
||||
// submit should be disabled
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
// submit should be disabled
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
|
||||
// put something in the password box, and hit enter; that should
|
||||
// trigger a request
|
||||
passwordNode.value = "s3kr3t";
|
||||
ReactTestUtils.Simulate.change(passwordNode);
|
||||
expect(submitNode.disabled).toBe(false);
|
||||
ReactTestUtils.Simulate.submit(formNode, {});
|
||||
// put something in the password box, and hit enter; that should
|
||||
// trigger a request
|
||||
passwordNode.value = "s3kr3t";
|
||||
ReactTestUtils.Simulate.change(passwordNode);
|
||||
expect(submitNode.disabled).toBe(false);
|
||||
ReactTestUtils.Simulate.submit(formNode, {});
|
||||
|
||||
expect(doRequest.callCount).toEqual(1);
|
||||
expect(doRequest.calledWithExactly({
|
||||
session: "sess",
|
||||
type: "m.login.password",
|
||||
password: "s3kr3t",
|
||||
user: "@user:id",
|
||||
})).toBe(true);
|
||||
expect(doRequest.callCount).toEqual(1);
|
||||
expect(doRequest.calledWithExactly({
|
||||
session: "sess",
|
||||
type: "m.login.password",
|
||||
password: "s3kr3t",
|
||||
user: "@user:id",
|
||||
})).toBe(true);
|
||||
|
||||
// there should now be a spinner
|
||||
ReactTestUtils.findRenderedComponentWithType(
|
||||
dlg, sdk.getComponent('elements.Spinner'),
|
||||
);
|
||||
// there should now be a spinner
|
||||
ReactTestUtils.findRenderedComponentWithType(
|
||||
dlg, sdk.getComponent('elements.Spinner'),
|
||||
);
|
||||
|
||||
// let the request complete
|
||||
q.delay(1).then(() => {
|
||||
// let the request complete
|
||||
return q.delay(1);
|
||||
}).then(() => {
|
||||
expect(onFinished.callCount).toEqual(1);
|
||||
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
||||
}).done(done, done);
|
||||
|
|
|
@ -1,11 +1,51 @@
|
|||
"use strict";
|
||||
|
||||
var sinon = require('sinon');
|
||||
var q = require('q');
|
||||
import sinon from 'sinon';
|
||||
import q from 'q';
|
||||
import ReactTestUtils from 'react-addons-test-utils';
|
||||
|
||||
var peg = require('../src/MatrixClientPeg.js');
|
||||
var jssdk = require('matrix-js-sdk');
|
||||
var MatrixEvent = jssdk.MatrixEvent;
|
||||
import peg from '../src/MatrixClientPeg.js';
|
||||
import jssdk from 'matrix-js-sdk';
|
||||
const MatrixEvent = jssdk.MatrixEvent;
|
||||
|
||||
/**
|
||||
* Wrapper around window.requestAnimationFrame that returns a promise
|
||||
* @private
|
||||
*/
|
||||
function _waitForFrame() {
|
||||
const def = q.defer();
|
||||
window.requestAnimationFrame(() => {
|
||||
def.resolve();
|
||||
});
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits a small number of animation frames for a component to appear
|
||||
* in the DOM. Like findRenderedDOMComponentWithTag(), but allows
|
||||
* for the element to appear a short time later, eg. if a promise needs
|
||||
* to resolve first.
|
||||
* @return a promise that resolves once the component appears, or rejects
|
||||
* if it doesn't appear after a nominal number of animation frames.
|
||||
*/
|
||||
export function waitForRenderedDOMComponentWithTag(tree, tag, attempts) {
|
||||
if (attempts === undefined) {
|
||||
// Let's start by assuming we'll only need to wait a single frame, and
|
||||
// we can try increasing this if necessary.
|
||||
attempts = 1;
|
||||
} else if (attempts == 0) {
|
||||
return q.reject("Gave up waiting for component with tag: " + tag);
|
||||
}
|
||||
|
||||
return _waitForFrame().then(() => {
|
||||
const result = ReactTestUtils.scryRenderedDOMComponentsWithTag(tree, tag);
|
||||
if (result.length > 0) {
|
||||
return result[0];
|
||||
} else {
|
||||
return waitForRenderedDOMComponentWithTag(tree, tag, attempts - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform common actions before each test case, e.g. printing the test case
|
||||
|
|
Loading…
Reference in a new issue