mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/hide-join-part-2
This commit is contained in:
commit
928294eba3
2
.babelrc
2
.babelrc
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["react", "es2015", "es2016"],
|
||||
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-generator", "transform-runtime", "add-module-exports"]
|
||||
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"]
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
src/AddThreepid.js
|
||||
src/async-components/views/dialogs/EncryptedEventDialog.js
|
||||
src/autocomplete/AutocompleteProvider.js
|
||||
src/autocomplete/Autocompleter.js
|
||||
@ -9,8 +8,6 @@ src/autocomplete/DuckDuckGoProvider.js
|
||||
src/autocomplete/EmojiProvider.js
|
||||
src/autocomplete/RoomProvider.js
|
||||
src/autocomplete/UserProvider.js
|
||||
src/Avatar.js
|
||||
src/BasePlatform.js
|
||||
src/CallHandler.js
|
||||
src/component-index.js
|
||||
src/components/structures/ContextualMenu.js
|
||||
@ -96,7 +93,6 @@ src/components/views/rooms/MessageComposerInput.js
|
||||
src/components/views/rooms/MessageComposerInputOld.js
|
||||
src/components/views/rooms/PresenceLabel.js
|
||||
src/components/views/rooms/ReadReceiptMarker.js
|
||||
src/components/views/rooms/RoomHeader.js
|
||||
src/components/views/rooms/RoomList.js
|
||||
src/components/views/rooms/RoomNameEditor.js
|
||||
src/components/views/rooms/RoomPreviewBar.js
|
||||
@ -115,16 +111,7 @@ src/components/views/settings/ChangePassword.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
src/components/views/settings/DevicesPanelEntry.js
|
||||
src/components/views/settings/EnableNotificationsButton.js
|
||||
src/components/views/voip/CallView.js
|
||||
src/components/views/voip/IncomingCallBox.js
|
||||
src/components/views/voip/VideoFeed.js
|
||||
src/components/views/voip/VideoView.js
|
||||
src/ContentMessages.js
|
||||
src/createRoom.js
|
||||
src/DateUtils.js
|
||||
src/email.js
|
||||
src/Entities.js
|
||||
src/extend.js
|
||||
src/HtmlUtils.js
|
||||
src/ImageUtils.js
|
||||
src/Invite.js
|
||||
@ -135,30 +122,20 @@ src/Markdown.js
|
||||
src/MatrixClientPeg.js
|
||||
src/Modal.js
|
||||
src/Notifier.js
|
||||
src/ObjectUtils.js
|
||||
src/PasswordReset.js
|
||||
src/PlatformPeg.js
|
||||
src/Presence.js
|
||||
src/ratelimitedfunc.js
|
||||
src/Resend.js
|
||||
src/RichText.js
|
||||
src/Roles.js
|
||||
src/RoomListSorter.js
|
||||
src/RoomNotifs.js
|
||||
src/Rooms.js
|
||||
src/ScalarAuthClient.js
|
||||
src/ScalarMessaging.js
|
||||
src/SdkConfig.js
|
||||
src/Skinner.js
|
||||
src/SlashCommands.js
|
||||
src/stores/LifecycleStore.js
|
||||
src/TabComplete.js
|
||||
src/TabCompleteEntries.js
|
||||
src/TextForEvent.js
|
||||
src/Tinter.js
|
||||
src/UiEffects.js
|
||||
src/Unread.js
|
||||
src/UserActivity.js
|
||||
src/utils/DecryptFile.js
|
||||
src/utils/DMRoomMap.js
|
||||
src/utils/FormattingUtils.js
|
||||
|
6
.flowconfig
Normal file
6
.flowconfig
Normal file
@ -0,0 +1,6 @@
|
||||
[include]
|
||||
src/**/*.js
|
||||
test/**/*.js
|
||||
|
||||
[ignore]
|
||||
node_modules/
|
@ -22,8 +22,11 @@ git checkout "$curbranch" || git checkout develop
|
||||
mkdir node_modules
|
||||
npm install
|
||||
|
||||
(cd node_modules/matrix-js-sdk && npm install)
|
||||
# use the version of js-sdk we just used in the react-sdk tests
|
||||
rm -r node_modules/matrix-js-sdk
|
||||
ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk
|
||||
|
||||
# ... and, of course, the version of react-sdk we just built
|
||||
rm -r node_modules/matrix-react-sdk
|
||||
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
||||
|
||||
|
@ -1,6 +1,15 @@
|
||||
# we need trusty for the chrome addon
|
||||
dist: trusty
|
||||
|
||||
# we don't need sudo, so can run in a container, which makes startup much
|
||||
# quicker.
|
||||
sudo: false
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- node # Latest stable version of nodejs.
|
||||
addons:
|
||||
chrome: stable
|
||||
install:
|
||||
- npm install
|
||||
- (cd node_modules/matrix-js-sdk && npm install)
|
||||
|
114
CHANGELOG.md
114
CHANGELOG.md
@ -1,3 +1,117 @@
|
||||
Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7)
|
||||
|
||||
* Fix ability to invite users with caps in their user IDs
|
||||
[\#1128](https://github.com/matrix-org/matrix-react-sdk/pull/1128)
|
||||
* Fix another race with first-sync
|
||||
[\#1131](https://github.com/matrix-org/matrix-react-sdk/pull/1131)
|
||||
* Make the indexeddb worker script work again
|
||||
[\#1132](https://github.com/matrix-org/matrix-react-sdk/pull/1132)
|
||||
* Use the web worker when clearing js-sdk stores
|
||||
[\#1133](https://github.com/matrix-org/matrix-react-sdk/pull/1133)
|
||||
|
||||
Changes in [0.9.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.6) (2017-06-20)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5...v0.9.6)
|
||||
|
||||
* Fix infinite spinner on email registration
|
||||
[\#1120](https://github.com/matrix-org/matrix-react-sdk/pull/1120)
|
||||
* Translate help promots in room list
|
||||
[\#1121](https://github.com/matrix-org/matrix-react-sdk/pull/1121)
|
||||
* Internationalise the drop targets
|
||||
[\#1122](https://github.com/matrix-org/matrix-react-sdk/pull/1122)
|
||||
* Fix another infinite spin on register
|
||||
[\#1124](https://github.com/matrix-org/matrix-react-sdk/pull/1124)
|
||||
|
||||
|
||||
Changes in [0.9.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5) (2017-06-19)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.2...v0.9.5)
|
||||
|
||||
* Don't peek when creating a room
|
||||
[\#1113](https://github.com/matrix-org/matrix-react-sdk/pull/1113)
|
||||
* More translations & translation fixes
|
||||
|
||||
|
||||
Changes in [0.9.5-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.2) (2017-06-16)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.1...v0.9.5-rc.2)
|
||||
|
||||
* Avoid getting stuck in a loop in CAS login
|
||||
[\#1109](https://github.com/matrix-org/matrix-react-sdk/pull/1109)
|
||||
* Update from Weblate.
|
||||
[\#1101](https://github.com/matrix-org/matrix-react-sdk/pull/1101)
|
||||
* Correctly inspect state when rejecting invite
|
||||
[\#1108](https://github.com/matrix-org/matrix-react-sdk/pull/1108)
|
||||
* Make sure to pass the roomAlias to the preview header if we have it
|
||||
[\#1107](https://github.com/matrix-org/matrix-react-sdk/pull/1107)
|
||||
* Make sure captcha disappears when container does
|
||||
[\#1106](https://github.com/matrix-org/matrix-react-sdk/pull/1106)
|
||||
* Fix URL previews
|
||||
[\#1105](https://github.com/matrix-org/matrix-react-sdk/pull/1105)
|
||||
|
||||
Changes in [0.9.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.1) (2017-06-15)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.4...v0.9.5-rc.1)
|
||||
|
||||
* Groundwork for tests including a teamserver login
|
||||
[\#1098](https://github.com/matrix-org/matrix-react-sdk/pull/1098)
|
||||
* Show a spinner when accepting an invite and waitingForRoom
|
||||
[\#1100](https://github.com/matrix-org/matrix-react-sdk/pull/1100)
|
||||
* Display a spinner until new room object after join success
|
||||
[\#1099](https://github.com/matrix-org/matrix-react-sdk/pull/1099)
|
||||
* Luke/attempt fix peeking regression
|
||||
[\#1097](https://github.com/matrix-org/matrix-react-sdk/pull/1097)
|
||||
* Show correct text in set email password dialog (2)
|
||||
[\#1096](https://github.com/matrix-org/matrix-react-sdk/pull/1096)
|
||||
* Don't create a guest login if user went to /login
|
||||
[\#1092](https://github.com/matrix-org/matrix-react-sdk/pull/1092)
|
||||
* Give password confirmation correct title, description
|
||||
[\#1095](https://github.com/matrix-org/matrix-react-sdk/pull/1095)
|
||||
* Make enter submit change password form
|
||||
[\#1094](https://github.com/matrix-org/matrix-react-sdk/pull/1094)
|
||||
* When not specified, remove roomAlias state in RoomViewStore
|
||||
[\#1093](https://github.com/matrix-org/matrix-react-sdk/pull/1093)
|
||||
* Update from Weblate.
|
||||
[\#1091](https://github.com/matrix-org/matrix-react-sdk/pull/1091)
|
||||
* Fixed pagination infinite loop caused by long messages
|
||||
[\#1045](https://github.com/matrix-org/matrix-react-sdk/pull/1045)
|
||||
* Clear persistent storage on login and logout
|
||||
[\#1085](https://github.com/matrix-org/matrix-react-sdk/pull/1085)
|
||||
* DM guessing: prefer oldest joined member
|
||||
[\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087)
|
||||
* Ask for email address after setting password for the first time
|
||||
[\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090)
|
||||
* i18n for setting password flow
|
||||
[\#1089](https://github.com/matrix-org/matrix-react-sdk/pull/1089)
|
||||
* remove mx_filterFlipColor from verified e2e icon so its not purple :/
|
||||
[\#1088](https://github.com/matrix-org/matrix-react-sdk/pull/1088)
|
||||
* width and height must be int otherwise synapse cries
|
||||
[\#1083](https://github.com/matrix-org/matrix-react-sdk/pull/1083)
|
||||
* remove RoomViewStore listener from MatrixChat on unmount
|
||||
[\#1084](https://github.com/matrix-org/matrix-react-sdk/pull/1084)
|
||||
* Add script to copy translations between files
|
||||
[\#1082](https://github.com/matrix-org/matrix-react-sdk/pull/1082)
|
||||
* Only process user_directory response if it's for the current query
|
||||
[\#1081](https://github.com/matrix-org/matrix-react-sdk/pull/1081)
|
||||
* Fix regressions with starting a 1-1.
|
||||
[\#1080](https://github.com/matrix-org/matrix-react-sdk/pull/1080)
|
||||
* allow forcing of TURN
|
||||
[\#1079](https://github.com/matrix-org/matrix-react-sdk/pull/1079)
|
||||
* Remove a bunch of dead code from react-sdk
|
||||
[\#1077](https://github.com/matrix-org/matrix-react-sdk/pull/1077)
|
||||
* Improve error logging/reporting in megolm import/export
|
||||
[\#1061](https://github.com/matrix-org/matrix-react-sdk/pull/1061)
|
||||
* Delinting
|
||||
[\#1064](https://github.com/matrix-org/matrix-react-sdk/pull/1064)
|
||||
* Show reason for a call hanging up unexpectedly.
|
||||
[\#1071](https://github.com/matrix-org/matrix-react-sdk/pull/1071)
|
||||
* Add reason for ban in room settings
|
||||
[\#1072](https://github.com/matrix-org/matrix-react-sdk/pull/1072)
|
||||
* adds mx_filterFlipColor so that the dark theme will invert this image
|
||||
[\#1070](https://github.com/matrix-org/matrix-react-sdk/pull/1070)
|
||||
|
||||
Changes in [0.9.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.4) (2017-06-14)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3...v0.9.4)
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
export KARMAFLAGS="--no-colors"
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 4
|
||||
@ -16,7 +15,7 @@ npm install
|
||||
(cd node_modules/matrix-js-sdk && npm install)
|
||||
|
||||
# run the mocha tests
|
||||
npm run test
|
||||
npm run test -- --no-colors
|
||||
|
||||
# run eslint
|
||||
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||
|
@ -116,11 +116,25 @@ module.exports = function (config) {
|
||||
browsers: [
|
||||
'Chrome',
|
||||
//'PhantomJS',
|
||||
//'ChromeHeadless',
|
||||
],
|
||||
|
||||
customLaunchers: {
|
||||
'ChromeHeadless': {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
// See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
|
||||
'--headless',
|
||||
'--disable-gpu',
|
||||
// Without a remote debugging port, Google Chrome exits immediately.
|
||||
'--remote-debugging-port=9222',
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
// singleRun: false,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
|
27
package.json
27
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.7",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
@ -33,28 +33,30 @@
|
||||
"scripts": {
|
||||
"reskindex": "node scripts/reskindex.js -h header",
|
||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||
"build": "npm run reskindex && babel src -d lib --source-maps",
|
||||
"build:watch": "babel src -w -d lib --source-maps",
|
||||
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
|
||||
"build:watch": "babel src -w -d lib --source-maps --copy-files",
|
||||
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
||||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||
"lint": "eslint src/",
|
||||
"lintall": "eslint src/ test/",
|
||||
"clean": "rimraf lib",
|
||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
||||
"test": "karma start $KARMAFLAGS --browsers PhantomJS",
|
||||
"test-multi": "karma start $KARMAFLAGS --single-run=false"
|
||||
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||
"test-multi": "karma start"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.11.6",
|
||||
"bluebird": "^3.5.0",
|
||||
"blueimp-canvas-to-blob": "^3.5.0",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"classnames": "^2.1.2",
|
||||
"commonmark": "^0.27.0",
|
||||
"counterpart": "^0.18.0",
|
||||
"draft-js": "^0.8.1",
|
||||
"draft-js": "^0.9.1",
|
||||
"draft-js-export-html": "^0.5.0",
|
||||
"draft-js-export-markdown": "^0.2.0",
|
||||
"emojione": "2.2.3",
|
||||
"emojione": "2.2.7",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "3.5.6",
|
||||
"flux": "2.1.1",
|
||||
@ -64,16 +66,16 @@
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.3",
|
||||
"lodash": "^4.13.1",
|
||||
"matrix-js-sdk": "0.7.11",
|
||||
"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",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"sanitize-html": "^1.14.1",
|
||||
"text-encoding-utf-8": "^1.0.1",
|
||||
"url": "^0.11.0",
|
||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
},
|
||||
@ -83,7 +85,7 @@
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-async-to-generator": "^6.16.0",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-class-properties": "^6.16.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
@ -105,12 +107,11 @@
|
||||
"karma-cli": "^0.1.2",
|
||||
"karma-junit-reporter": "^0.4.1",
|
||||
"karma-mocha": "^0.2.2",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"matrix-react-test-utils": "^0.1.1",
|
||||
"mocha": "^2.4.5",
|
||||
"parallelshell": "^1.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"react-addons-test-utils": "^15.4.0",
|
||||
"require-json": "0.0.1",
|
||||
"rimraf": "^2.4.3",
|
||||
|
26
scripts/emoji-data-strip.js
Normal file
26
scripts/emoji-data-strip.js
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
const EMOJI_DATA = require('emojione/emoji.json');
|
||||
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
|
||||
const fs = require('fs');
|
||||
|
||||
const output = Object.keys(EMOJI_DATA).map(
|
||||
(key) => {
|
||||
const datum = EMOJI_DATA[key];
|
||||
const newDatum = {
|
||||
name: datum.name,
|
||||
shortname: datum.shortname,
|
||||
category: datum.category,
|
||||
emoji_order: datum.emoji_order,
|
||||
};
|
||||
if (datum.aliases_ascii.length > 0) {
|
||||
newDatum.aliases_ascii = datum.aliases_ascii;
|
||||
}
|
||||
return newDatum;
|
||||
}
|
||||
).filter((datum) => {
|
||||
return EMOJI_SUPPORTED.includes(datum.shortname);
|
||||
});
|
||||
|
||||
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||
// babel using --copy-files
|
||||
fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output));
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
/**
|
||||
@ -44,7 +44,7 @@ class AddThreepid {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||
if (err.errcode === 'M_THREEPID_IN_USE') {
|
||||
err.message = _t('This email address is already in use');
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
@ -69,7 +69,7 @@ class AddThreepid {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||
if (err.errcode === 'M_THREEPID_IN_USE') {
|
||||
err.message = _t('This phone number is already in use');
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
@ -85,16 +85,15 @@ class AddThreepid {
|
||||
* the request failed.
|
||||
*/
|
||||
checkEmailLinkClicked() {
|
||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||
return MatrixClientPeg.get().addThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: identityServerDomain
|
||||
id_server: identityServerDomain,
|
||||
}, this.bind).catch(function(err) {
|
||||
if (err.httpStatus === 401) {
|
||||
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
} else if (err.httpStatus) {
|
||||
err.message += ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
@ -104,6 +103,7 @@ class AddThreepid {
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} token phone number verification code as entered by the user
|
||||
* @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.
|
||||
@ -119,7 +119,7 @@ class AddThreepid {
|
||||
return MatrixClientPeg.get().addThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: identityServerDomain
|
||||
id_server: identityServerDomain,
|
||||
}, this.bind);
|
||||
});
|
||||
}
|
||||
|
@ -15,18 +15,18 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
import {ContentRepo} from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
|
||||
module.exports = {
|
||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||
var url = member.getAvatarUrl(
|
||||
let url = member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
);
|
||||
if (!url) {
|
||||
// member can be null here currently since on invites, the JS SDK
|
||||
@ -38,11 +38,11 @@ module.exports = {
|
||||
},
|
||||
|
||||
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||
var url = ContentRepo.getHttpUriForMxc(
|
||||
const url = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod
|
||||
resizeMethod,
|
||||
);
|
||||
if (!url || url.length === 0) {
|
||||
return null;
|
||||
@ -51,11 +51,11 @@ module.exports = {
|
||||
},
|
||||
|
||||
defaultAvatarUrlForString: function(s) {
|
||||
var images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||
var total = 0;
|
||||
for (var i = 0; i < s.length; ++i) {
|
||||
const images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||
let total = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
total += s.charCodeAt(i);
|
||||
}
|
||||
return 'img/' + images[total % images.length] + '.png';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -17,6 +17,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import dis from './dispatcher';
|
||||
|
||||
/**
|
||||
* Base class for classes that provide platform-specific functionality
|
||||
* eg. Setting an application badge or displaying notifications
|
||||
@ -27,6 +29,16 @@ export default class BasePlatform {
|
||||
constructor() {
|
||||
this.notificationCount = 0;
|
||||
this.errorDidOccur = false;
|
||||
|
||||
dis.register(this._onAction.bind(this));
|
||||
}
|
||||
|
||||
_onAction(payload: Object) {
|
||||
switch (payload.action) {
|
||||
case 'on_logged_out':
|
||||
this.setNotificationCount(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Used primarily for Analytics
|
||||
@ -45,6 +57,7 @@ export default class BasePlatform {
|
||||
/**
|
||||
* Returns true if the platform supports displaying
|
||||
* notifications, otherwise false.
|
||||
* @returns {boolean} whether the platform supports displaying notifications
|
||||
*/
|
||||
supportsNotifications(): boolean {
|
||||
return false;
|
||||
@ -53,6 +66,7 @@ export default class BasePlatform {
|
||||
/**
|
||||
* Returns true if the application currently has permission
|
||||
* to display notifications. Otherwise false.
|
||||
* @returns {boolean} whether the application has permission to display notifications
|
||||
*/
|
||||
maySendNotifications(): boolean {
|
||||
return false;
|
||||
|
82
src/ComposerHistoryManager.js
Normal file
82
src/ComposerHistoryManager.js
Normal file
@ -0,0 +1,82 @@
|
||||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ContentState} from 'draft-js';
|
||||
import * as RichText from './RichText';
|
||||
import Markdown from './Markdown';
|
||||
import _flow from 'lodash/flow';
|
||||
import _clamp from 'lodash/clamp';
|
||||
|
||||
type MessageFormat = 'html' | 'markdown';
|
||||
|
||||
class HistoryItem {
|
||||
message: string = '';
|
||||
format: MessageFormat = 'html';
|
||||
|
||||
constructor(message: string, format: MessageFormat) {
|
||||
this.message = message;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
toContentState(format: MessageFormat): ContentState {
|
||||
let {message} = this;
|
||||
if (format === 'markdown') {
|
||||
if (this.format === 'html') {
|
||||
message = _flow([RichText.htmlToContentState, RichText.stateToMarkdown])(message);
|
||||
}
|
||||
return ContentState.createFromText(message);
|
||||
} else {
|
||||
if (this.format === 'markdown') {
|
||||
message = new Markdown(message).toHTML();
|
||||
}
|
||||
return RichText.htmlToContentState(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class ComposerHistoryManager {
|
||||
history: Array<HistoryItem> = [];
|
||||
prefix: string;
|
||||
lastIndex: number = 0;
|
||||
currentIndex: number = 0;
|
||||
|
||||
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||
this.prefix = prefix + roomId;
|
||||
|
||||
// TODO: Performance issues?
|
||||
let item;
|
||||
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||
this.history.push(
|
||||
Object.assign(new HistoryItem(), JSON.parse(item)),
|
||||
);
|
||||
}
|
||||
this.lastIndex = this.currentIndex;
|
||||
}
|
||||
|
||||
addItem(message: string, format: MessageFormat) {
|
||||
const item = new HistoryItem(message, format);
|
||||
this.history.push(item);
|
||||
this.currentIndex = this.lastIndex + 1;
|
||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
|
||||
}
|
||||
|
||||
getItem(offset: number, format: MessageFormat): ?ContentState {
|
||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
|
||||
const item = this.history[this.currentIndex];
|
||||
return item ? item.toContentState(format) : null;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var q = require('q');
|
||||
import Promise from 'bluebird';
|
||||
var extend = require('./extend');
|
||||
var dis = require('./dispatcher');
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
@ -52,7 +52,7 @@ const MAX_HEIGHT = 600;
|
||||
* and a thumbnail key.
|
||||
*/
|
||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
const deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
|
||||
var targetWidth = inputWidth;
|
||||
var targetHeight = inputHeight;
|
||||
@ -95,7 +95,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
* @return {Promise} A promise that resolves with the html image element.
|
||||
*/
|
||||
function loadImageElement(imageFile) {
|
||||
const deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
|
||||
// Load the file into an html element
|
||||
const img = document.createElement("img");
|
||||
@ -154,7 +154,7 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
*/
|
||||
function loadVideoElement(videoFile) {
|
||||
const deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
|
||||
// Load the file into an html element
|
||||
const video = document.createElement("video");
|
||||
@ -210,7 +210,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||
* is read.
|
||||
*/
|
||||
function readFileAsArrayBuffer(file) {
|
||||
const deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
deferred.resolve(e.target.result);
|
||||
@ -229,11 +229,13 @@ function readFileAsArrayBuffer(file) {
|
||||
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
||||
* @param {String} roomId The ID of the room being uploaded to.
|
||||
* @param {File} file The file to upload.
|
||||
* @param {Function?} progressHandler optional callback to be called when a chunk of
|
||||
* data is uploaded.
|
||||
* @return {Promise} A promise that resolves with an object.
|
||||
* If the file is unencrypted then the object will have a "url" key.
|
||||
* If the file is encrypted then the object will have a "file" key.
|
||||
*/
|
||||
function uploadFile(matrixClient, roomId, file) {
|
||||
function uploadFile(matrixClient, roomId, file, progressHandler) {
|
||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||
// If the room is encrypted then encrypt the file before uploading it.
|
||||
// First read the file into memory.
|
||||
@ -245,7 +247,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||
const encryptInfo = encryptResult.info;
|
||||
// Pass the encrypted data as a Blob to the uploader.
|
||||
const blob = new Blob([encryptResult.data]);
|
||||
return matrixClient.uploadContent(blob).then(function(url) {
|
||||
return matrixClient.uploadContent(blob, {
|
||||
progressHandler: progressHandler,
|
||||
}).then(function(url) {
|
||||
// If the attachment is encrypted then bundle the URL along
|
||||
// with the information needed to decrypt the attachment and
|
||||
// add it under a file key.
|
||||
@ -257,7 +261,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const basePromise = matrixClient.uploadContent(file);
|
||||
const basePromise = matrixClient.uploadContent(file, {
|
||||
progressHandler: progressHandler,
|
||||
});
|
||||
const promise1 = basePromise.then(function(url) {
|
||||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return {"url": url};
|
||||
@ -288,7 +294,7 @@ class ContentMessages {
|
||||
content.info.mimetype = file.type;
|
||||
}
|
||||
|
||||
const def = q.defer();
|
||||
const def = Promise.defer();
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
||||
@ -326,23 +332,24 @@ class ContentMessages {
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
||||
var error;
|
||||
|
||||
function onProgress(ev) {
|
||||
upload.total = ev.total;
|
||||
upload.loaded = ev.loaded;
|
||||
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||
}
|
||||
|
||||
return def.promise.then(function() {
|
||||
// XXX: upload.promise must be the promise that
|
||||
// is returned by uploadFile as it has an abort()
|
||||
// method hacked onto it.
|
||||
upload.promise = uploadFile(
|
||||
matrixClient, roomId, file
|
||||
matrixClient, roomId, file, onProgress,
|
||||
);
|
||||
return upload.promise.then(function(result) {
|
||||
content.file = result.file;
|
||||
content.url = result.url;
|
||||
});
|
||||
}).progress(function(ev) {
|
||||
if (ev) {
|
||||
upload.total = ev.total;
|
||||
upload.loaded = ev.loaded;
|
||||
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||
}
|
||||
}).then(function(url) {
|
||||
return matrixClient.sendMessage(roomId, content);
|
||||
}, function(err) {
|
||||
|
@ -54,24 +54,25 @@ function pad(n) {
|
||||
function twelveHourTime(date) {
|
||||
let hours = date.getHours() % 12;
|
||||
const minutes = pad(date.getMinutes());
|
||||
const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
|
||||
hours = pad(hours ? hours : 12);
|
||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||
hours = hours ? hours : 12; // convert 0 -> 12
|
||||
return `${hours}:${minutes}${ampm}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatDate: function(date) {
|
||||
var now = new Date();
|
||||
formatDate: function(date, showTwelveHour=false) {
|
||||
const now = new Date();
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return this.formatTime(date);
|
||||
}
|
||||
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date)});
|
||||
}
|
||||
else if (now.getFullYear() === date.getFullYear()) {
|
||||
return _t('%(weekDayName)s %(time)s', {
|
||||
weekDayName: days[date.getDay()],
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
});
|
||||
} else if (now.getFullYear() === date.getFullYear()) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
|
||||
weekDayName: days[date.getDay()],
|
||||
@ -80,7 +81,7 @@ module.exports = {
|
||||
time: this.formatTime(date),
|
||||
});
|
||||
}
|
||||
return this.formatFullDate(date);
|
||||
return this.formatFullDate(date, showTwelveHour);
|
||||
},
|
||||
|
||||
formatFullDate: function(date, showTwelveHour=false) {
|
||||
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('./index');
|
||||
import sdk from './index';
|
||||
|
||||
function isMatch(query, name, uid) {
|
||||
query = query.toLowerCase();
|
||||
@ -33,8 +32,8 @@ function isMatch(query, name, uid) {
|
||||
}
|
||||
|
||||
// split spaces in name and try matching constituent parts
|
||||
var parts = name.split(" ");
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
const parts = name.split(" ");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i].indexOf(query) === 0) {
|
||||
return true;
|
||||
}
|
||||
@ -67,7 +66,7 @@ class Entity {
|
||||
|
||||
class MemberEntity extends Entity {
|
||||
getJsx() {
|
||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
return (
|
||||
<MemberTile key={this.model.userId} member={this.model} />
|
||||
);
|
||||
@ -84,6 +83,7 @@ class UserEntity extends Entity {
|
||||
super(model);
|
||||
this.showInviteButton = Boolean(showInviteButton);
|
||||
this.inviteFn = inviteFn;
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
@ -93,15 +93,15 @@ class UserEntity extends Entity {
|
||||
}
|
||||
|
||||
getJsx() {
|
||||
var UserTile = sdk.getComponent("rooms.UserTile");
|
||||
const UserTile = sdk.getComponent("rooms.UserTile");
|
||||
return (
|
||||
<UserTile key={this.model.userId} user={this.model}
|
||||
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
|
||||
showInviteButton={this.showInviteButton} onClick={this.onClick} />
|
||||
);
|
||||
}
|
||||
|
||||
matches(queryString) {
|
||||
var name = this.model.displayName || this.model.userId;
|
||||
const name = this.model.displayName || this.model.userId;
|
||||
return isMatch(queryString, name, this.model.userId);
|
||||
}
|
||||
}
|
||||
@ -109,7 +109,7 @@ class UserEntity extends Entity {
|
||||
|
||||
module.exports = {
|
||||
newEntity: function(jsx, matchFn) {
|
||||
var entity = new Entity();
|
||||
const entity = new Entity();
|
||||
entity.getJsx = function() {
|
||||
return jsx;
|
||||
};
|
||||
@ -137,5 +137,5 @@ module.exports = {
|
||||
return users.map(function(u) {
|
||||
return new UserEntity(u, showInviteButton, inviteFn);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -23,6 +23,7 @@ var linkifyMatrix = require('./linkify-matrix');
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
@ -37,7 +38,7 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
* because we want to include emoji shortnames in title text
|
||||
*/
|
||||
export function unicodeToImage(str) {
|
||||
let replaceWith, unicode, alt;
|
||||
let replaceWith, unicode, alt, short, fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
@ -49,11 +50,14 @@ export function unicodeToImage(str) {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
short = mappedUnicode[unicode];
|
||||
fname = emojione.emojioneList[short].fname;
|
||||
|
||||
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
|
||||
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||
const title = mappedUnicode[unicode];
|
||||
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${unicode}.svg${emojione.cacheBustParam}"/>`;
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
return replaceWith;
|
||||
}
|
||||
});
|
||||
@ -84,7 +88,7 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||
}
|
||||
|
||||
|
||||
export function stripParagraphs(html: string): string {
|
||||
export function processHtmlForSending(html: string): string {
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.innerHTML = html;
|
||||
|
||||
@ -93,10 +97,21 @@ export function stripParagraphs(html: string): string {
|
||||
}
|
||||
|
||||
let contentHTML = "";
|
||||
for (let i=0; i<contentDiv.children.length; i++) {
|
||||
for (let i=0; i < contentDiv.children.length; i++) {
|
||||
const element = contentDiv.children[i];
|
||||
if (element.tagName.toLowerCase() === 'p') {
|
||||
contentHTML += element.innerHTML + '<br />';
|
||||
contentHTML += element.innerHTML;
|
||||
// Don't add a <br /> for the last <p>
|
||||
if (i !== contentDiv.children.length - 1) {
|
||||
contentHTML += '<br />';
|
||||
}
|
||||
} else if (element.tagName.toLowerCase() === 'pre') {
|
||||
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
|
||||
// redundant. This is a workaround for a bug in draft-js-export-html:
|
||||
// https://github.com/sstur/draft-js-export-html/issues/62
|
||||
contentHTML += '<pre>' +
|
||||
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
|
||||
'</pre>';
|
||||
} else {
|
||||
const temp = document.createElement('div');
|
||||
temp.appendChild(element.cloneNode(true));
|
||||
@ -107,33 +122,39 @@ export function stripParagraphs(html: string): string {
|
||||
return contentHTML;
|
||||
}
|
||||
|
||||
var sanitizeHtmlParams = {
|
||||
/*
|
||||
* Given an untrusted HTML string, return a React node with an sanitized version
|
||||
* of that HTML.
|
||||
*/
|
||||
export function sanitizedHtmlNode(insaneHtml) {
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
|
||||
const sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'del', // for markdown
|
||||
'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',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||
],
|
||||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||
// We don't currently allow img itself by default, but this
|
||||
// would make sense if we did
|
||||
img: ['src'],
|
||||
ol: ['start'],
|
||||
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
|
||||
},
|
||||
// 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'],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
||||
|
||||
// DO NOT USE. sanitize-html allows all URL starting with '//'
|
||||
// so this will always allow links to whatever scheme the
|
||||
// host page is served over.
|
||||
allowedSchemesByTag: {},
|
||||
allowProtocolRelative: false,
|
||||
|
||||
transformTags: { // custom to matrix
|
||||
// add blank targets to all hyperlinks except vector URLs
|
||||
@ -165,6 +186,33 @@ var sanitizeHtmlParams = {
|
||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
return { tagName: tagName, attribs : attribs };
|
||||
},
|
||||
'img': function(tagName, attribs) {
|
||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||
// we don't want to allow images with `https?` `src`s.
|
||||
if (!attribs.src.startsWith('mxc://')) {
|
||||
return { tagName, attribs: {}};
|
||||
}
|
||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
attribs.src,
|
||||
attribs.width || 800,
|
||||
attribs.height || 600,
|
||||
);
|
||||
return { tagName: tagName, attribs: attribs };
|
||||
},
|
||||
'code': function(tagName, attribs) {
|
||||
if (typeof attribs.class !== 'undefined') {
|
||||
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||
let classes = attribs.class.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-');
|
||||
});
|
||||
attribs.class = classes.join(' ');
|
||||
}
|
||||
return {
|
||||
tagName: tagName,
|
||||
attribs: attribs,
|
||||
};
|
||||
},
|
||||
'*': function(tagName, attribs) {
|
||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||
// because attributes are stripped after transforming
|
||||
|
@ -21,6 +21,7 @@ module.exports = {
|
||||
ENTER: 13,
|
||||
SHIFT: 16,
|
||||
ESCAPE: 27,
|
||||
SPACE: 32,
|
||||
PAGE_UP: 33,
|
||||
PAGE_DOWN: 34,
|
||||
END: 35,
|
||||
@ -30,7 +31,30 @@ module.exports = {
|
||||
RIGHT: 39,
|
||||
DOWN: 40,
|
||||
DELETE: 46,
|
||||
KEY_A: 65,
|
||||
KEY_B: 66,
|
||||
KEY_C: 67,
|
||||
KEY_D: 68,
|
||||
KEY_E: 69,
|
||||
KEY_F: 70,
|
||||
KEY_G: 71,
|
||||
KEY_H: 72,
|
||||
KEY_I: 73,
|
||||
KEY_J: 74,
|
||||
KEY_K: 75,
|
||||
KEY_L: 76,
|
||||
KEY_M: 77,
|
||||
KEY_N: 78,
|
||||
KEY_O: 79,
|
||||
KEY_P: 80,
|
||||
KEY_Q: 81,
|
||||
KEY_R: 82,
|
||||
KEY_S: 83,
|
||||
KEY_T: 84,
|
||||
KEY_U: 85,
|
||||
KEY_V: 86,
|
||||
KEY_W: 87,
|
||||
KEY_X: 88,
|
||||
KEY_Y: 89,
|
||||
KEY_Z: 90,
|
||||
};
|
||||
|
138
src/KeyRequestHandler.js
Normal file
138
src/KeyRequestHandler.js
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
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 sdk from './index';
|
||||
import Modal from './Modal';
|
||||
|
||||
export default class KeyRequestHandler {
|
||||
constructor(matrixClient) {
|
||||
this._matrixClient = matrixClient;
|
||||
|
||||
// the user/device for which we currently have a dialog open
|
||||
this._currentUser = null;
|
||||
this._currentDevice = null;
|
||||
|
||||
// userId -> deviceId -> [keyRequest]
|
||||
this._pendingKeyRequests = Object.create(null);
|
||||
}
|
||||
|
||||
handleKeyRequest(keyRequest) {
|
||||
const userId = keyRequest.userId;
|
||||
const deviceId = keyRequest.deviceId;
|
||||
const requestId = keyRequest.requestId;
|
||||
|
||||
if (!this._pendingKeyRequests[userId]) {
|
||||
this._pendingKeyRequests[userId] = Object.create(null);
|
||||
}
|
||||
if (!this._pendingKeyRequests[userId][deviceId]) {
|
||||
this._pendingKeyRequests[userId][deviceId] = [];
|
||||
}
|
||||
|
||||
// check if we already have this request
|
||||
const requests = this._pendingKeyRequests[userId][deviceId];
|
||||
if (requests.find((r) => r.requestId === requestId)) {
|
||||
console.log("Already have this key request, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
requests.push(keyRequest);
|
||||
|
||||
if (this._currentUser) {
|
||||
// ignore for now
|
||||
console.log("Key request, but we already have a dialog open");
|
||||
return;
|
||||
}
|
||||
|
||||
this._processNextRequest();
|
||||
}
|
||||
|
||||
handleKeyRequestCancellation(cancellation) {
|
||||
// see if we can find the request in the queue
|
||||
const userId = cancellation.userId;
|
||||
const deviceId = cancellation.deviceId;
|
||||
const requestId = cancellation.requestId;
|
||||
|
||||
if (userId === this._currentUser && deviceId === this._currentDevice) {
|
||||
console.log(
|
||||
"room key request cancellation for the user we currently have a"
|
||||
+ " dialog open for",
|
||||
);
|
||||
// TODO: update the dialog. For now, we just ignore the
|
||||
// cancellation.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._pendingKeyRequests[userId]) {
|
||||
return;
|
||||
}
|
||||
const requests = this._pendingKeyRequests[userId][deviceId];
|
||||
if (!requests) {
|
||||
return;
|
||||
}
|
||||
const idx = requests.findIndex((r) => r.requestId === requestId);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
console.log("Forgetting room key request");
|
||||
requests.splice(idx, 1);
|
||||
if (requests.length === 0) {
|
||||
delete this._pendingKeyRequests[userId][deviceId];
|
||||
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
|
||||
delete this._pendingKeyRequests[userId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_processNextRequest() {
|
||||
const userId = Object.keys(this._pendingKeyRequests)[0];
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const deviceId = Object.keys(this._pendingKeyRequests[userId])[0];
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`);
|
||||
|
||||
const finished = (r) => {
|
||||
this._currentUser = null;
|
||||
this._currentDevice = null;
|
||||
|
||||
if (r) {
|
||||
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
||||
req.share();
|
||||
}
|
||||
}
|
||||
delete this._pendingKeyRequests[userId][deviceId];
|
||||
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
|
||||
delete this._pendingKeyRequests[userId];
|
||||
}
|
||||
|
||||
this._processNextRequest();
|
||||
};
|
||||
|
||||
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
||||
Modal.createDialog(KeyShareDialog, {
|
||||
matrixClient: this._matrixClient,
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
onFinished: finished,
|
||||
});
|
||||
this._currentUser = userId;
|
||||
this._currentDevice = deviceId;
|
||||
}
|
||||
}
|
||||
|
172
src/Lifecycle.js
172
src/Lifecycle.js
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
@ -29,32 +29,25 @@ import DMRoomMap from './utils/DMRoomMap';
|
||||
import RtsClient from './RtsClient';
|
||||
import Modal from './Modal';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
* a number of things:
|
||||
*
|
||||
* 1. if we have a loginToken in the (real) query params, it uses that to log
|
||||
* in.
|
||||
*
|
||||
* 2. if we have a guest access token in the fragment query params, it uses
|
||||
* 1. if we have a guest access token in the fragment query params, it uses
|
||||
* that.
|
||||
*
|
||||
* 3. if an access token is stored in local storage (from a previous session),
|
||||
* 2. if an access token is stored in local storage (from a previous session),
|
||||
* it uses that.
|
||||
*
|
||||
* 4. it attempts to auto-register as a guest user.
|
||||
* 3. it attempts to auto-register as a guest user.
|
||||
*
|
||||
* If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
|
||||
* turn will raise on_logged_in and will_start_client events.
|
||||
*
|
||||
* @param {object} opts
|
||||
*
|
||||
* @param {object} opts.realQueryParams: string->string map of the
|
||||
* query-parameters extracted from the real query-string of the starting
|
||||
* URI.
|
||||
*
|
||||
* @param {object} opts.fragmentQueryParams: string->string map of the
|
||||
* query-parameters extracted from the #-fragment of the starting URI.
|
||||
*
|
||||
@ -68,9 +61,10 @@ import { _t } from './languageHandler';
|
||||
* true; defines the IS to use.
|
||||
*
|
||||
* @returns {Promise} a promise which resolves when the above process completes.
|
||||
* Resolves to `true` if we ended up starting a session, or `false` if we
|
||||
* failed.
|
||||
*/
|
||||
export function loadSession(opts) {
|
||||
const realQueryParams = opts.realQueryParams || {};
|
||||
const fragmentQueryParams = opts.fragmentQueryParams || {};
|
||||
let enableGuest = opts.enableGuest || false;
|
||||
const guestHsUrl = opts.guestHsUrl;
|
||||
@ -82,14 +76,6 @@ export function loadSession(opts) {
|
||||
enableGuest = false;
|
||||
}
|
||||
|
||||
if (realQueryParams.loginToken) {
|
||||
if (!realQueryParams.homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
} else {
|
||||
return _loginWithToken(realQueryParams, defaultDeviceDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableGuest &&
|
||||
fragmentQueryParams.guest_user_id &&
|
||||
fragmentQueryParams.guest_access_token
|
||||
@ -101,12 +87,12 @@ export function loadSession(opts) {
|
||||
homeserverUrl: guestHsUrl,
|
||||
identityServerUrl: guestIsUrl,
|
||||
guest: true,
|
||||
}, true);
|
||||
}, true).then(() => true);
|
||||
}
|
||||
|
||||
return _restoreFromLocalStorage().then((success) => {
|
||||
if (success) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (enableGuest) {
|
||||
@ -114,10 +100,30 @@ export function loadSession(opts) {
|
||||
}
|
||||
|
||||
// fall back to login screen
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||
/**
|
||||
* @param {Object} queryParams string->string map of the
|
||||
* query-parameters extracted from the real query-string of the starting
|
||||
* URI.
|
||||
*
|
||||
* @param {String} defaultDeviceDisplayName
|
||||
*
|
||||
* @returns {Promise} promise which resolves to true if we completed the token
|
||||
* login, else false
|
||||
*/
|
||||
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||
if (!queryParams.loginToken) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (!queryParams.homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: queryParams.homeserver,
|
||||
@ -130,22 +136,26 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||
},
|
||||
).then(function(data) {
|
||||
console.log("Logged in with token");
|
||||
return _doSetLoggedIn({
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
homeserverUrl: queryParams.homeserver,
|
||||
identityServerUrl: queryParams.identityServer,
|
||||
guest: false,
|
||||
}, true);
|
||||
}, (err) => {
|
||||
return _clearStorage().then(() => {
|
||||
_persistCredentialsToLocalStorage({
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
homeserverUrl: queryParams.homeserver,
|
||||
identityServerUrl: queryParams.identityServer,
|
||||
guest: false,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error("Failed to log in with login token: " + err + " " +
|
||||
err.data);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
console.log("Doing guest login on %s", hsUrl);
|
||||
console.log(`Doing guest login on ${hsUrl}`);
|
||||
|
||||
// TODO: we should probably de-duplicate this and Login.loginAsGuest.
|
||||
// Not really sure where the right home for it is.
|
||||
@ -160,7 +170,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
initial_device_display_name: defaultDeviceDisplayName,
|
||||
},
|
||||
}).then((creds) => {
|
||||
console.log("Registered as guest: %s", creds.user_id);
|
||||
console.log(`Registered as guest: ${creds.user_id}`);
|
||||
return _doSetLoggedIn({
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
@ -168,9 +178,10 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
guest: true,
|
||||
}, true);
|
||||
}, true).then(() => true);
|
||||
}, (err) => {
|
||||
console.error("Failed to register as guest: " + err + " " + err.data);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -186,7 +197,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
// localStorage (e.g. teamToken, isGuest etc.)
|
||||
function _restoreFromLocalStorage() {
|
||||
if (!localStorage) {
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
@ -203,7 +214,7 @@ function _restoreFromLocalStorage() {
|
||||
}
|
||||
|
||||
if (accessToken && userId && hsUrl) {
|
||||
console.log("Restoring session for %s", userId);
|
||||
console.log(`Restoring session for ${userId}`);
|
||||
try {
|
||||
return _doSetLoggedIn({
|
||||
userId: userId,
|
||||
@ -218,34 +229,19 @@ function _restoreFromLocalStorage() {
|
||||
}
|
||||
} else {
|
||||
console.log("No previous session found.");
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
function _handleRestoreFailure(e) {
|
||||
console.log("Unable to restore session", e);
|
||||
|
||||
let msg = e.message;
|
||||
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
|
||||
msg = _t(
|
||||
'You need to log back in to generate end-to-end encryption keys'
|
||||
+ ' for this device and submit the public key to your homeserver.'
|
||||
+ ' This is a once off; sorry for the inconvenience.',
|
||||
);
|
||||
|
||||
_clearStorage();
|
||||
|
||||
return q.reject(new Error(
|
||||
_t('Unable to restore previous session') + ': ' + msg,
|
||||
));
|
||||
}
|
||||
|
||||
const def = q.defer();
|
||||
const def = Promise.defer();
|
||||
const SessionRestoreErrorDialog =
|
||||
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
||||
|
||||
Modal.createDialog(SessionRestoreErrorDialog, {
|
||||
error: msg,
|
||||
error: e.message,
|
||||
onFinished: (success) => {
|
||||
def.resolve(success);
|
||||
},
|
||||
@ -282,10 +278,12 @@ export function initRtsClient(url) {
|
||||
* storage before starting the new client.
|
||||
*
|
||||
* @param {MatrixClientCreds} credentials The credentials to use
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export function setLoggedIn(credentials) {
|
||||
stopMatrixClient();
|
||||
_doSetLoggedIn(credentials, true);
|
||||
return _doSetLoggedIn(credentials, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,23 +293,26 @@ export function setLoggedIn(credentials) {
|
||||
* @param {MatrixClientCreds} credentials
|
||||
* @param {Boolean} clearStorage
|
||||
*
|
||||
* returns a Promise which resolves once the client has been started
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
|
||||
console.log(
|
||||
"setLoggedIn: mxid:", credentials.userId,
|
||||
"deviceId:", credentials.deviceId,
|
||||
"guest:", credentials.guest,
|
||||
"hs:", credentials.homeserverUrl,
|
||||
"setLoggedIn: mxid: " + credentials.userId +
|
||||
" deviceId: " + credentials.deviceId +
|
||||
" guest: " + credentials.guest +
|
||||
" hs: " + credentials.homeserverUrl,
|
||||
);
|
||||
|
||||
// This is dispatched to indicate that the user is still in the process of logging in
|
||||
// because `teamPromise` may take some time to resolve, breaking the assumption that
|
||||
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
|
||||
// later than MatrixChat might assume.
|
||||
dis.dispatch({action: 'on_logging_in'});
|
||||
//
|
||||
// we fire it *synchronously* to make sure it fires before on_logged_in.
|
||||
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
||||
dis.dispatch({action: 'on_logging_in'}, true);
|
||||
|
||||
if (clearStorage) {
|
||||
await _clearStorage();
|
||||
@ -322,23 +323,10 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
// Resolves by default
|
||||
let teamPromise = Promise.resolve(null);
|
||||
|
||||
// persist the session
|
||||
|
||||
if (localStorage) {
|
||||
try {
|
||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
// if we didn't get a deviceId from the login, leave mx_device_id unset,
|
||||
// rather than setting it to "undefined".
|
||||
//
|
||||
// (in this case MatrixClient doesn't bother with the crypto stuff
|
||||
// - that's fine for us).
|
||||
if (credentials.deviceId) {
|
||||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
}
|
||||
_persistCredentialsToLocalStorage(credentials);
|
||||
|
||||
// The user registered as a PWLU (PassWord-Less User), the generated password
|
||||
// is cached here such that the user can change it at a later time.
|
||||
@ -349,8 +337,6 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
cachedPassword: credentials.password,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Session persisted for %s", credentials.userId);
|
||||
} catch (e) {
|
||||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
@ -361,6 +347,9 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
localStorage.setItem("mx_team_token", body.team_token);
|
||||
}
|
||||
return body.team_token;
|
||||
}, (err) => {
|
||||
console.warn(`Failed to get team token on login: ${err}` );
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -371,12 +360,29 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
|
||||
teamPromise.then((teamToken) => {
|
||||
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||
}, (err) => {
|
||||
console.warn("Failed to get team token on login", err);
|
||||
dis.dispatch({action: 'on_logged_in', teamToken: null});
|
||||
});
|
||||
|
||||
startMatrixClient();
|
||||
return MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
// if we didn't get a deviceId from the login, leave mx_device_id unset,
|
||||
// rather than setting it to "undefined".
|
||||
//
|
||||
// (in this case MatrixClient doesn't bother with the crypto stuff
|
||||
// - that's fine for us).
|
||||
if (credentials.deviceId) {
|
||||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
}
|
||||
|
||||
console.log(`Session persisted for ${credentials.userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -416,6 +422,8 @@ export function logout() {
|
||||
* listen for events while a session is logged in.
|
||||
*/
|
||||
function startMatrixClient() {
|
||||
console.log(`Lifecycle: Starting MatrixClient`);
|
||||
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
// to add listeners for the 'sync' event so otherwise we'd have
|
||||
// a race condition (and we need to dispatch synchronously for this
|
||||
|
19
src/Login.js
19
src/Login.js
@ -18,7 +18,7 @@ limitations under the License.
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import { _t } from "./languageHandler";
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import url from 'url';
|
||||
|
||||
export default class Login {
|
||||
@ -144,7 +144,7 @@ export default class Login {
|
||||
|
||||
const client = this._createTemporaryClient();
|
||||
return client.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
@ -160,7 +160,7 @@ export default class Login {
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return q({
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
@ -178,11 +178,18 @@ export default class Login {
|
||||
}
|
||||
|
||||
redirectToCas() {
|
||||
var client = this._createTemporaryClient();
|
||||
var parsedUrl = url.parse(window.location.href, true);
|
||||
const client = this._createTemporaryClient();
|
||||
const parsedUrl = url.parse(window.location.href, true);
|
||||
|
||||
// XXX: at this point, the fragment will always be #/login, which is no
|
||||
// use to anyone. Ideally, we would get the intended fragment from
|
||||
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||
// through a CAS login.
|
||||
parsedUrl.hash = "";
|
||||
|
||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
||||
const casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
||||
window.location.href = casUrl;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
import commonmark from 'commonmark';
|
||||
import escape from 'lodash/escape';
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['del'];
|
||||
const ALLOWED_HTML_TAGS = ['del', 'u'];
|
||||
|
||||
// These types of node are definitely text
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
@ -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.
|
||||
@ -47,7 +48,6 @@ class MatrixClientPeg {
|
||||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
this.indexedDbWorkerScript = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,7 +58,7 @@ class MatrixClientPeg {
|
||||
* @param {string} script href to the script to be passed to the web worker
|
||||
*/
|
||||
setIndexedDbWorkerScript(script) {
|
||||
this.indexedDbWorkerScript = script;
|
||||
createMatrixClient.indexedDbWorkerScript = script;
|
||||
}
|
||||
|
||||
get(): MatrixClient {
|
||||
@ -77,20 +77,38 @@ class MatrixClientPeg {
|
||||
this._createClient(creds);
|
||||
}
|
||||
|
||||
start() {
|
||||
async start() {
|
||||
// try to initialise e2e on the new client
|
||||
try {
|
||||
// check that we have a version of the js-sdk which includes initCrypto
|
||||
if (this.matrixClient.initCrypto) {
|
||||
await this.matrixClient.initCrypto();
|
||||
}
|
||||
} catch(e) {
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
// that the olm library was missing. It's not fatal.
|
||||
console.warn("Unable to initialise e2e: " + e);
|
||||
}
|
||||
|
||||
const opts = utils.deepCopy(this.opts);
|
||||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
|
||||
let promise = this.matrixClient.store.startup();
|
||||
// log any errors when starting up the database (if one exists)
|
||||
promise.catch((err) => { console.error(err); });
|
||||
try {
|
||||
let promise = this.matrixClient.store.startup();
|
||||
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
||||
await promise;
|
||||
} catch(err) {
|
||||
// log any errors when starting up the database (if one exists)
|
||||
console.error(`Error starting matrixclient store: ${err}`);
|
||||
}
|
||||
|
||||
// regardless of errors, start the client. If we did error out, we'll
|
||||
// just end up doing a full initial /sync.
|
||||
promise.finally(() => {
|
||||
this.get().startClient(opts);
|
||||
});
|
||||
|
||||
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
this.get().startClient(opts);
|
||||
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||
}
|
||||
|
||||
getCredentials(): MatrixClientCreds {
|
||||
@ -127,7 +145,7 @@ class MatrixClientPeg {
|
||||
timelineSupport: true,
|
||||
};
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
|
||||
|
||||
// we're going to add eventlisteners for each matrix event tile, so the
|
||||
// potential number of event listeners is quite high.
|
||||
|
@ -23,8 +23,8 @@ limitations under the License.
|
||||
* { key: $KEY, val: $VALUE, place: "add|del" }
|
||||
*/
|
||||
module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||
var results = [];
|
||||
var delta = {};
|
||||
const results = [];
|
||||
const delta = {};
|
||||
Object.keys(before).forEach(function(beforeKey) {
|
||||
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
|
||||
delta[beforeKey]--; // keys present in the past have -ve values
|
||||
@ -46,9 +46,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||
results.push({ place: "del", key: muxedKey, val: beforeVal });
|
||||
});
|
||||
break;
|
||||
case 0: // A mix of added/removed keys
|
||||
case 0: {// A mix of added/removed keys
|
||||
// compare old & new vals
|
||||
var itemDelta = {};
|
||||
const itemDelta = {};
|
||||
before[muxedKey].forEach(function(beforeVal) {
|
||||
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
|
||||
itemDelta[beforeVal]--;
|
||||
@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error("Calculated key delta of " + delta[muxedKey] +
|
||||
" - this should never happen!");
|
||||
console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Shallow-compare two objects for equality: each key and value must be
|
||||
* identical
|
||||
* Shallow-compare two objects for equality: each key and value must be identical
|
||||
* @param {Object} objA First object to compare against the second
|
||||
* @param {Object} objB Second object to compare against the first
|
||||
* @return {boolean} whether the two objects have same key=values
|
||||
*/
|
||||
module.exports.shallowEqual = function(objA, objB) {
|
||||
if (objA === objB) {
|
||||
@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var keysA = Object.keys(objA);
|
||||
var keysB = Object.keys(objB);
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < keysA.length; i++) {
|
||||
var key = keysA[i];
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
const key = keysA[i];
|
||||
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
|
||||
return false;
|
||||
}
|
||||
|
@ -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.
|
||||
@ -22,4 +23,6 @@ export default {
|
||||
CreateRoom: "create_room",
|
||||
RoomDirectory: "room_directory",
|
||||
UserView: "user_view",
|
||||
GroupView: "group_view",
|
||||
MyGroups: "my_groups",
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
/**
|
||||
@ -34,7 +34,7 @@ class PasswordReset {
|
||||
constructor(homeserverUrl, identityUrl) {
|
||||
this.client = Matrix.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
idBaseUrl: identityUrl
|
||||
idBaseUrl: identityUrl,
|
||||
});
|
||||
this.clientSecret = this.client.generateClientSecret();
|
||||
this.identityServerDomain = identityUrl.split("://")[1];
|
||||
@ -53,7 +53,7 @@ class PasswordReset {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_THREEPID_NOT_FOUND') {
|
||||
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
|
||||
err.message = _t('This email address was not found');
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
@ -75,16 +75,15 @@ class PasswordReset {
|
||||
threepid_creds: {
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: this.identityServerDomain
|
||||
}
|
||||
id_server: this.identityServerDomain,
|
||||
},
|
||||
}, this.password).catch(function(err) {
|
||||
if (err.httpStatus === 401) {
|
||||
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||
}
|
||||
else if (err.httpStatus === 404) {
|
||||
err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
} else if (err.httpStatus === 404) {
|
||||
err.message =
|
||||
_t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
|
||||
} else if (err.httpStatus) {
|
||||
err.message += ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
var dis = require('./dispatcher');
|
||||
var sdk = require('./index');
|
||||
var Modal = require('./Modal');
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import dis from './dispatcher';
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
|
||||
module.exports = {
|
||||
@ -37,12 +35,10 @@ module.exports = {
|
||||
},
|
||||
resend: function(event) {
|
||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
MatrixClientPeg.get().resendEvent(
|
||||
event, room
|
||||
).done(function(res) {
|
||||
MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent',
|
||||
event: event
|
||||
event: event,
|
||||
});
|
||||
}, function(err) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
@ -58,7 +54,7 @@ module.exports = {
|
||||
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
event: event
|
||||
event: event,
|
||||
});
|
||||
});
|
||||
},
|
||||
@ -66,7 +62,7 @@ module.exports = {
|
||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||
dis.dispatch({
|
||||
action: 'message_send_cancelled',
|
||||
event: event
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ import * as sdk from './index';
|
||||
import * as emojione from 'emojione';
|
||||
import {stateToHTML} from 'draft-js-export-html';
|
||||
import {SelectionRange} from "./autocomplete/Autocompleter";
|
||||
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||
|
||||
const MARKDOWN_REGEX = {
|
||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||
@ -30,9 +31,26 @@ const USERNAME_REGEX = /@\S+:\S+/g;
|
||||
const ROOM_REGEX = /#\S+:\S+/g;
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
||||
|
||||
export const contentStateToHTML = stateToHTML;
|
||||
const ZWS_CODE = 8203;
|
||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||
export function stateToMarkdown(state) {
|
||||
return __stateToMarkdown(state)
|
||||
.replace(
|
||||
ZWS, // draft-js-export-markdown adds these
|
||||
''); // this is *not* a zero width space, trust me :)
|
||||
}
|
||||
|
||||
export function HTMLtoContentState(html: string): ContentState {
|
||||
export const contentStateToHTML = (contentState: ContentState) => {
|
||||
return stateToHTML(contentState, {
|
||||
inlineStyles: {
|
||||
UNDERLINE: {
|
||||
element: 'u'
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function htmlToContentState(html: string): ContentState {
|
||||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||
}
|
||||
|
||||
@ -95,31 +113,6 @@ let emojiDecorator = {
|
||||
* Returns a composite decorator which has access to provided scope.
|
||||
*/
|
||||
export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||
let MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
||||
let usernameDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(USERNAME_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
let member = scope.room.getMember(props.children[0].props.text);
|
||||
// unused until we make these decorators immutable (autocomplete needed)
|
||||
let name = member ? member.name : null;
|
||||
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
|
||||
return <span className="mx_UserPill">{avatar}{props.children}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
let roomDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(ROOM_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
return <span className="mx_RoomPill">{props.children}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO Re-enable usernameDecorator and roomDecorator
|
||||
return [emojiDecorator];
|
||||
}
|
||||
|
||||
@ -146,9 +139,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
</a>
|
||||
)
|
||||
});
|
||||
markdownDecorators.push(emojiDecorator);
|
||||
|
||||
return markdownDecorators;
|
||||
// markdownDecorators.push(emojiDecorator);
|
||||
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||
return [emojiDecorator];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -286,3 +279,14 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||
|
||||
return editorState;
|
||||
}
|
||||
|
||||
export function hasMultiLineSelection(editorState: EditorState): boolean {
|
||||
const selectionState = editorState.getSelection();
|
||||
const anchorKey = selectionState.getAnchorKey();
|
||||
const currentContent = editorState.getCurrentContent();
|
||||
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
|
||||
const start = selectionState.getStartOffset();
|
||||
const end = selectionState.getEndOffset();
|
||||
const selectedText = currentContentBlock.getText().slice(start, end);
|
||||
return selectedText.includes('\n');
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export function levelRoleMap() {
|
||||
return {
|
||||
undefined: _t('Default'),
|
||||
0: _t('User'),
|
||||
50: _t('Moderator'),
|
||||
50: _t('Moderator'),
|
||||
100: _t('Admin'),
|
||||
};
|
||||
}
|
||||
|
@ -19,8 +19,7 @@ limitations under the License.
|
||||
function tsOfNewestEvent(room) {
|
||||
if (room.timeline.length) {
|
||||
return room.timeline[room.timeline.length - 1].getTs();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
}
|
||||
@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mostRecentActivityFirst: mostRecentActivityFirst
|
||||
mostRecentActivityFirst,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
|
||||
export const ALL_MESSAGES = 'all_messages';
|
||||
@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) {
|
||||
}
|
||||
|
||||
export function setRoomNotifsState(roomId, newState) {
|
||||
if (newState == MUTE) {
|
||||
if (newState === MUTE) {
|
||||
return setRoomNotifsStateMuted(roomId);
|
||||
} else {
|
||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||
@ -80,14 +80,14 @@ function setRoomNotifsStateMuted(roomId) {
|
||||
kind: 'event_match',
|
||||
key: 'room_id',
|
||||
pattern: roomId,
|
||||
}
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
'dont_notify',
|
||||
]
|
||||
],
|
||||
}));
|
||||
|
||||
return q.all(promises);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
||||
}
|
||||
|
||||
if (newState == 'all_messages') {
|
||||
if (newState === 'all_messages') {
|
||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||
if (roomRule) {
|
||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
||||
}
|
||||
} else if (newState == 'mentions_only') {
|
||||
} else if (newState === 'mentions_only') {
|
||||
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||
actions: [
|
||||
'dont_notify',
|
||||
]
|
||||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
@ -119,14 +119,14 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
{
|
||||
set_tweak: 'sound',
|
||||
value: 'default',
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}));
|
||||
// https://matrix.org/jira/browse/SPEC-400
|
||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
}
|
||||
|
||||
return q.all(promises);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function findOverrideMuteRule(roomId) {
|
||||
@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) {
|
||||
return false;
|
||||
}
|
||||
const cond = rule.conditions[0];
|
||||
if (
|
||||
cond.kind == 'event_match' &&
|
||||
cond.key == 'room_id' &&
|
||||
cond.pattern == roomId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
||||
}
|
||||
|
||||
function isMuteRule(rule) {
|
||||
return (
|
||||
rule.actions.length == 1 &&
|
||||
rule.actions[0] == 'dont_notify'
|
||||
);
|
||||
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
* Given a room object, return the alias we should use for it,
|
||||
@ -102,7 +102,7 @@ export function guessAndSetDMRoom(room, isDirect) {
|
||||
*/
|
||||
export function setDMRoom(roomId, userId) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return q();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var request = require('browser-request');
|
||||
|
||||
var SdkConfig = require('./SdkConfig');
|
||||
@ -39,7 +39,7 @@ class ScalarAuthClient {
|
||||
// Returns a scalar_token string
|
||||
getScalarToken() {
|
||||
var tok = window.localStorage.getItem("mx_scalar_token");
|
||||
if (tok) return q(tok);
|
||||
if (tok) return Promise.resolve(tok);
|
||||
|
||||
// No saved token, so do the dance to get one. First, we
|
||||
// need an openid bearer token from the HS.
|
||||
@ -53,7 +53,7 @@ class ScalarAuthClient {
|
||||
}
|
||||
|
||||
exchangeForScalarToken(openid_token_object) {
|
||||
var defer = q.defer();
|
||||
var defer = Promise.defer();
|
||||
|
||||
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||
request({
|
||||
@ -76,10 +76,13 @@ class ScalarAuthClient {
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
getScalarInterfaceUrlForRoom(roomId) {
|
||||
getScalarInterfaceUrlForRoom(roomId, screen) {
|
||||
var url = SdkConfig.get().integrations_ui_url;
|
||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||
url += "&room_id=" + encodeURIComponent(roomId);
|
||||
if (screen) {
|
||||
url += '&screen=' + encodeURIComponent(screen);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@ -89,4 +92,3 @@ class ScalarAuthClient {
|
||||
}
|
||||
|
||||
module.exports = ScalarAuthClient;
|
||||
|
||||
|
@ -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.
|
||||
@ -17,7 +18,7 @@ limitations under the License.
|
||||
/*
|
||||
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
|
||||
{
|
||||
action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
|
||||
action: "invite" | "membership_state" | "bot_options" | "set_bot_options" | etc... ,
|
||||
room_id: $ROOM_ID,
|
||||
user_id: $USER_ID
|
||||
// additional request fields
|
||||
@ -109,6 +110,99 @@ Example:
|
||||
response: 78
|
||||
}
|
||||
|
||||
can_send_event
|
||||
--------------
|
||||
Check if the client can send the given event into the given room. If the client
|
||||
is unable to do this, an error response is returned instead of 'response: false'.
|
||||
|
||||
Request:
|
||||
- room_id is the room to do the check in.
|
||||
- event_type is the event type which will be sent.
|
||||
- is_state is true if the event to be sent is a state event.
|
||||
Response:
|
||||
true
|
||||
Example:
|
||||
{
|
||||
action: "can_send_event",
|
||||
is_state: false,
|
||||
event_type: "m.room.message",
|
||||
room_id: "!foo:bar",
|
||||
response: true
|
||||
}
|
||||
|
||||
set_widget
|
||||
----------
|
||||
Set a new widget in the room. Clobbers based on the ID.
|
||||
|
||||
Request:
|
||||
- `room_id` (String) is the room to set the widget in.
|
||||
- `widget_id` (String) is the ID of the widget to add (or replace if it already exists).
|
||||
It can be an arbitrary UTF8 string and is purely for distinguishing between widgets.
|
||||
- `url` (String) is the URL that clients should load in an iframe to run the widget.
|
||||
All widgets must have a valid URL. If the URL is `null` (not `undefined`), the
|
||||
widget will be removed from the room.
|
||||
- `type` (String) is the type of widget, which is provided as a hint for matrix clients so they
|
||||
can configure/lay out the widget in different ways. All widgets must have a type.
|
||||
- `name` (String) is an optional human-readable string about the widget.
|
||||
- `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs.
|
||||
Response:
|
||||
{
|
||||
success: true
|
||||
}
|
||||
Example:
|
||||
{
|
||||
action: "set_widget",
|
||||
room_id: "!foo:bar",
|
||||
widget_id: "abc123",
|
||||
url: "http://widget.url",
|
||||
type: "example",
|
||||
response: {
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
get_widgets
|
||||
-----------
|
||||
Get a list of all widgets in the room. The response is an array
|
||||
of state events.
|
||||
|
||||
Request:
|
||||
- `room_id` (String) is the room to get the widgets in.
|
||||
Response:
|
||||
[
|
||||
{
|
||||
type: "im.vector.modular.widgets",
|
||||
state_key: "wid1",
|
||||
content: {
|
||||
type: "grafana",
|
||||
url: "https://grafanaurl",
|
||||
name: "dashboard",
|
||||
data: {key: "val"}
|
||||
}
|
||||
room_id: “!foo:bar”,
|
||||
sender: "@alice:localhost"
|
||||
}
|
||||
]
|
||||
Example:
|
||||
{
|
||||
action: "get_widgets",
|
||||
room_id: "!foo:bar",
|
||||
response: [
|
||||
{
|
||||
type: "im.vector.modular.widgets",
|
||||
state_key: "wid1",
|
||||
content: {
|
||||
type: "grafana",
|
||||
url: "https://grafanaurl",
|
||||
name: "dashboard",
|
||||
data: {key: "val"}
|
||||
}
|
||||
room_id: “!foo:bar”,
|
||||
sender: "@alice:localhost"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
membership_state AND bot_options
|
||||
--------------------------------
|
||||
@ -191,6 +285,87 @@ function inviteUser(event, roomId, userId) {
|
||||
});
|
||||
}
|
||||
|
||||
function setWidget(event, roomId) {
|
||||
const widgetId = event.data.widget_id;
|
||||
const widgetType = event.data.type;
|
||||
const widgetUrl = event.data.url;
|
||||
const widgetName = event.data.name; // optional
|
||||
const widgetData = event.data.data; // optional
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// both adding/removing widgets need these checks
|
||||
if (!widgetId || widgetUrl === undefined) {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
|
||||
// check types of fields
|
||||
if (widgetName !== undefined && typeof widgetName !== 'string') {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
|
||||
return;
|
||||
}
|
||||
if (widgetData !== undefined && !(widgetData instanceof Object)) {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
|
||||
return;
|
||||
}
|
||||
if (typeof widgetType !== 'string') {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
|
||||
return;
|
||||
}
|
||||
if (typeof widgetUrl !== 'string') {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let content = {
|
||||
type: widgetType,
|
||||
url: widgetUrl,
|
||||
name: widgetName,
|
||||
data: widgetData,
|
||||
};
|
||||
if (widgetUrl === null) { // widget is being deleted
|
||||
content = {};
|
||||
}
|
||||
|
||||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
||||
sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
}, (err) => {
|
||||
sendError(event, _t('Failed to send request.'), err);
|
||||
});
|
||||
}
|
||||
|
||||
function getWidgets(event, roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
sendError(event, _t('This room is not recognised.'));
|
||||
return;
|
||||
}
|
||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
// Only return widgets which have required fields
|
||||
let widgetStateEvents = [];
|
||||
stateEvents.forEach((ev) => {
|
||||
if (ev.getContent().type && ev.getContent().url) {
|
||||
widgetStateEvents.push(ev.event); // return the raw event
|
||||
}
|
||||
})
|
||||
|
||||
sendResponse(event, widgetStateEvents);
|
||||
}
|
||||
|
||||
function setPlumbingState(event, roomId, status) {
|
||||
if (typeof status !== 'string') {
|
||||
throw new Error('Plumbing state status should be a string');
|
||||
@ -287,6 +462,42 @@ function getMembershipCount(event, roomId) {
|
||||
sendResponse(event, count);
|
||||
}
|
||||
|
||||
function canSendEvent(event, roomId) {
|
||||
const evType = "" + event.data.event_type; // force stringify
|
||||
const isState = Boolean(event.data.is_state);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
sendError(event, _t('This room is not recognised.'));
|
||||
return;
|
||||
}
|
||||
const me = client.credentials.userId;
|
||||
const member = room.getMember(me);
|
||||
if (!member || member.membership !== "join") {
|
||||
sendError(event, _t('You are not in this room.'));
|
||||
return;
|
||||
}
|
||||
|
||||
let canSend = false;
|
||||
if (isState) {
|
||||
canSend = room.currentState.maySendStateEvent(evType, me);
|
||||
}
|
||||
else {
|
||||
canSend = room.currentState.maySendEvent(evType, me);
|
||||
}
|
||||
|
||||
if (!canSend) {
|
||||
sendError(event, _t('You do not have permission to do that in this room.'));
|
||||
return;
|
||||
}
|
||||
|
||||
sendResponse(event, true);
|
||||
}
|
||||
|
||||
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
@ -332,7 +543,7 @@ const onMessage = function(event) {
|
||||
// All strings start with the empty string, so for sanity return if the length
|
||||
// of the event origin is 0.
|
||||
let url = SdkConfig.get().integrations_ui_url;
|
||||
if (event.origin.length === 0 || !url.startsWith(event.origin)) {
|
||||
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
|
||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
@ -367,7 +578,7 @@ const onMessage = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting join rules does not require userId
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
@ -377,6 +588,15 @@ const onMessage = function(event) {
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
getMembershipCount(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
setWidget(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_widgets") {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
@ -409,12 +629,27 @@ const onMessage = function(event) {
|
||||
});
|
||||
};
|
||||
|
||||
let listenerCount = 0;
|
||||
module.exports = {
|
||||
startListening: function() {
|
||||
window.addEventListener("message", onMessage, false);
|
||||
if (listenerCount === 0) {
|
||||
window.addEventListener("message", onMessage, false);
|
||||
}
|
||||
listenerCount += 1;
|
||||
},
|
||||
|
||||
stopListening: function() {
|
||||
window.removeEventListener("message", onMessage);
|
||||
listenerCount -= 1;
|
||||
if (listenerCount === 0) {
|
||||
window.removeEventListener("message", onMessage);
|
||||
}
|
||||
if (listenerCount < 0) {
|
||||
// Make an error so we get a stack trace
|
||||
const e = new Error(
|
||||
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
||||
" Negative count"
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var DEFAULTS = {
|
||||
const DEFAULTS = {
|
||||
// URL to a page we show in an iframe to configure integrations
|
||||
integrations_ui_url: "https://scalar.vector.im/",
|
||||
// Base URL to the REST interface of the integrations server
|
||||
@ -30,8 +30,8 @@ class SdkConfig {
|
||||
}
|
||||
|
||||
static put(cfg) {
|
||||
var defaultKeys = Object.keys(DEFAULTS);
|
||||
for (var i = 0; i < defaultKeys.length; ++i) {
|
||||
const defaultKeys = Object.keys(DEFAULTS);
|
||||
for (let i = 0; i < defaultKeys.length; ++i) {
|
||||
if (cfg[defaultKeys[i]] === undefined) {
|
||||
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
|
||||
}
|
||||
|
@ -51,19 +51,18 @@ class Skinner {
|
||||
if (this.components !== null) {
|
||||
throw new Error(
|
||||
"Attempted to load a skin while a skin is already loaded"+
|
||||
"If you want to change the active skin, call resetSkin first"
|
||||
);
|
||||
"If you want to change the active skin, call resetSkin first");
|
||||
}
|
||||
this.components = {};
|
||||
var compKeys = Object.keys(skinObject.components);
|
||||
for (var i = 0; i < compKeys.length; ++i) {
|
||||
var comp = skinObject.components[compKeys[i]];
|
||||
const compKeys = Object.keys(skinObject.components);
|
||||
for (let i = 0; i < compKeys.length; ++i) {
|
||||
const comp = skinObject.components[compKeys[i]];
|
||||
this.addComponent(compKeys[i], comp);
|
||||
}
|
||||
}
|
||||
|
||||
addComponent(name, comp) {
|
||||
var slot = name;
|
||||
let slot = name;
|
||||
if (comp.replaces !== undefined) {
|
||||
if (comp.replaces.indexOf('.') > -1) {
|
||||
slot = comp.replaces;
|
||||
|
@ -186,7 +186,7 @@ const commands = {
|
||||
if (targetRoomId) { break; }
|
||||
}
|
||||
if (!targetRoomId) {
|
||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,52 +301,54 @@ const commands = {
|
||||
const deviceId = matches[2];
|
||||
const fingerprint = matches[3];
|
||||
|
||||
const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
return success(
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
|
||||
if (!device) {
|
||||
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
|
||||
if (device.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
return reject(_t(`Device already verified!`));
|
||||
} else {
|
||||
return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
||||
}
|
||||
}
|
||||
if (device.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new Error(_t(`Device already verified!`));
|
||||
} else {
|
||||
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
||||
}
|
||||
}
|
||||
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
userId, deviceId, true,
|
||||
);
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new Error(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
||||
}
|
||||
|
||||
// Tell the user we verified everything!
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Verified key"),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
{
|
||||
_t("The signing key you provided matches the signing key you received " +
|
||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
||||
{userId: userId, deviceId: deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
|
||||
return success();
|
||||
} else {
|
||||
const fprint = device.getFingerprint();
|
||||
return reject(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
|
||||
);
|
||||
}
|
||||
return MatrixClientPeg.get().setDeviceVerified(
|
||||
userId, deviceId, true,
|
||||
);
|
||||
}).then(() => {
|
||||
// Tell the user we verified everything
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Verified key"),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
{
|
||||
_t("The signing key you provided matches the signing key you received " +
|
||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
||||
{userId: userId, deviceId: deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
|
@ -1,391 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries';
|
||||
import SlashCommands from './SlashCommands';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
|
||||
const DELAY_TIME_MS = 1000;
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
// NB: DO NOT USE \b its "words" are roman alphabet only!
|
||||
//
|
||||
// Capturing group containing the start
|
||||
// of line or a whitespace char
|
||||
// \_______________ __________Capturing group of 0 or more non-whitespace chars
|
||||
// _|__ _|_ followed by the end of line
|
||||
// / \/ \
|
||||
const MATCH_REGEX = /(^|\s)(\S*)$/;
|
||||
|
||||
class TabComplete {
|
||||
|
||||
constructor(opts) {
|
||||
opts.allowLooping = opts.allowLooping || false;
|
||||
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
|
||||
opts.onClickCompletes = opts.onClickCompletes || false;
|
||||
this.opts = opts;
|
||||
this.completing = false;
|
||||
this.list = []; // full set of tab-completable things
|
||||
this.matchedList = []; // subset of completable things to loop over
|
||||
this.currentIndex = 0; // index in matchedList currently
|
||||
this.originalText = null; // original input text when tab was first hit
|
||||
this.textArea = opts.textArea; // DOMElement
|
||||
this.isFirstWord = false; // true if you tab-complete on the first word
|
||||
this.enterTabCompleteTimerId = null;
|
||||
this.inPassiveMode = false;
|
||||
|
||||
// Map tracking ordering of the room members.
|
||||
// userId: integer, highest comes first.
|
||||
this.memberTabOrder = {};
|
||||
|
||||
// monotonically increasing counter used for tracking ordering of members
|
||||
this.memberOrderSeq = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when a a UI element representing a tab complete entry has been clicked
|
||||
* @param {entry} The entry that was clicked
|
||||
*/
|
||||
onEntryClick(entry) {
|
||||
if (this.opts.onClickCompletes) {
|
||||
this.completeTo(entry);
|
||||
}
|
||||
}
|
||||
|
||||
loadEntries(room) {
|
||||
this._makeEntries(room);
|
||||
this._initSorting(room);
|
||||
this._sortEntries();
|
||||
}
|
||||
|
||||
onMemberSpoke(member) {
|
||||
if (this.memberTabOrder[member.userId] === undefined) {
|
||||
this.list.push(new MemberEntry(member));
|
||||
}
|
||||
this.memberTabOrder[member.userId] = this.memberOrderSeq++;
|
||||
this._sortEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement}
|
||||
*/
|
||||
setTextArea(textArea) {
|
||||
this.textArea = textArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isTabCompleting() {
|
||||
// actually have things to tab over
|
||||
return this.completing && this.matchedList.length > 1;
|
||||
}
|
||||
|
||||
stopTabCompleting() {
|
||||
this.completing = false;
|
||||
this.currentIndex = 0;
|
||||
this._notifyStateChange();
|
||||
}
|
||||
|
||||
startTabCompleting(passive) {
|
||||
this.originalText = this.textArea.value; // cache starting text
|
||||
|
||||
// grab the partial word from the text which we'll be tab-completing
|
||||
var res = MATCH_REGEX.exec(this.originalText);
|
||||
if (!res) {
|
||||
this.matchedList = [];
|
||||
return;
|
||||
}
|
||||
// ES6 destructuring; ignore first element (the complete match)
|
||||
var [, boundaryGroup, partialGroup] = res;
|
||||
|
||||
if (partialGroup.length === 0 && passive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFirstWord = partialGroup.length === this.originalText.length;
|
||||
|
||||
this.completing = true;
|
||||
this.currentIndex = 0;
|
||||
|
||||
this.matchedList = [
|
||||
new Entry(partialGroup) // first entry is always the original partial
|
||||
];
|
||||
|
||||
// find matching entries in the set of entries given to us
|
||||
this.list.forEach((entry) => {
|
||||
if (entry.text.toLowerCase().indexOf(partialGroup.toLowerCase()) === 0) {
|
||||
this.matchedList.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("calculated completions => %s", JSON.stringify(this.matchedList));
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an auto-complete with the given word. This terminates the tab-complete.
|
||||
* @param {Entry} entry The tab-complete entry to complete to.
|
||||
*/
|
||||
completeTo(entry) {
|
||||
this.textArea.value = this._replaceWith(
|
||||
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
|
||||
);
|
||||
this.stopTabCompleting();
|
||||
// keep focus on the text area
|
||||
this.textArea.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} numAheadToPeek Return *up to* this many elements.
|
||||
* @return {Entry[]}
|
||||
*/
|
||||
peek(numAheadToPeek) {
|
||||
if (this.matchedList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
var peekList = [];
|
||||
|
||||
// return the current match item and then one with an index higher, and
|
||||
// so on until we've reached the requested limit. If we hit the end of
|
||||
// the list of options we're done.
|
||||
for (var i = 0; i < numAheadToPeek; i++) {
|
||||
var nextIndex;
|
||||
if (this.opts.allowLooping) {
|
||||
nextIndex = (this.currentIndex + i) % this.matchedList.length;
|
||||
}
|
||||
else {
|
||||
nextIndex = this.currentIndex + i;
|
||||
if (nextIndex === this.matchedList.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
peekList.push(this.matchedList[nextIndex]);
|
||||
}
|
||||
// console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList));
|
||||
return peekList;
|
||||
}
|
||||
|
||||
handleTabPress(passive, shiftKey) {
|
||||
var wasInPassiveMode = this.inPassiveMode && !passive;
|
||||
this.inPassiveMode = passive;
|
||||
|
||||
if (!this.completing) {
|
||||
this.startTabCompleting(passive);
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
this.nextMatchedEntry(-1);
|
||||
}
|
||||
else {
|
||||
// if we were in passive mode we got out of sync by incrementing the
|
||||
// index to show the peek view but not set the text area. Therefore,
|
||||
// we want to set the *current* index rather than the *next* index.
|
||||
this.nextMatchedEntry(wasInPassiveMode ? 0 : 1);
|
||||
}
|
||||
this._notifyStateChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMEvent} e
|
||||
*/
|
||||
onKeyDown(ev) {
|
||||
if (!this.textArea) {
|
||||
console.error("onKeyDown called before a <textarea> was set!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.keyCode !== KEY_TAB) {
|
||||
// pressing any key (except shift, windows, cmd (OSX) and ctrl/alt combinations)
|
||||
// aborts the current tab completion
|
||||
if (this.completing && ev.keyCode !== KEY_SHIFT &&
|
||||
!ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS) {
|
||||
// they're resuming typing; reset tab complete state vars.
|
||||
this.stopTabCompleting();
|
||||
}
|
||||
|
||||
|
||||
// explicitly pressing any key except tab removes passive mode. Tab doesn't remove
|
||||
// passive mode because handleTabPress needs to know when passive mode is toggling
|
||||
// off so it can resync the textarea/peek list. If tab did remove passive mode then
|
||||
// handleTabPress would never be able to tell when passive mode toggled off.
|
||||
this.inPassiveMode = false;
|
||||
|
||||
// pressing any key at all (except tab) restarts the automatic tab-complete timer
|
||||
if (this.opts.autoEnterTabComplete) {
|
||||
const cachedText = ev.target.value;
|
||||
clearTimeout(this.enterTabCompleteTimerId);
|
||||
this.enterTabCompleteTimerId = setTimeout(() => {
|
||||
if (this.completing) {
|
||||
// If you highlight text and CTRL+X it, tab-completing will not be reset.
|
||||
// This check makes sure that if something like a cut operation has been
|
||||
// done, that we correctly refresh the tab-complete list. Normal backspace
|
||||
// operations get caught by the stopTabCompleting() section above, but
|
||||
// because the CTRL key is held, this does not execute for CTRL+X.
|
||||
if (cachedText !== this.textArea.value) {
|
||||
this.stopTabCompleting();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.completing) {
|
||||
this.handleTabPress(true, false);
|
||||
}
|
||||
}, DELAY_TIME_MS);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ctrl-tab/alt-tab etc shouldn't trigger a complete
|
||||
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
||||
|
||||
// tab key has been pressed at this point
|
||||
this.handleTabPress(false, ev.shiftKey);
|
||||
|
||||
// prevent the default TAB operation (typically focus shifting)
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the textarea to the next value in the matched list.
|
||||
* @param {Number} offset Offset to apply *before* setting the next value.
|
||||
*/
|
||||
nextMatchedEntry(offset) {
|
||||
if (this.matchedList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// work out the new index, wrapping if necessary.
|
||||
this.currentIndex += offset;
|
||||
if (this.currentIndex >= this.matchedList.length) {
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
else if (this.currentIndex < 0) {
|
||||
this.currentIndex = this.matchedList.length - 1;
|
||||
}
|
||||
var isTransitioningToOriginalText = (
|
||||
// impossible to transition if they've never hit tab
|
||||
!this.inPassiveMode && this.currentIndex === 0
|
||||
);
|
||||
|
||||
if (!this.inPassiveMode) {
|
||||
// set textarea to this new value
|
||||
this.textArea.value = this._replaceWith(
|
||||
this.matchedList[this.currentIndex].getFillText(),
|
||||
this.currentIndex !== 0, // don't suffix the original text!
|
||||
this.matchedList[this.currentIndex].getSuffix(this.isFirstWord)
|
||||
);
|
||||
}
|
||||
|
||||
// visual display to the user that we looped - TODO: This should be configurable
|
||||
if (isTransitioningToOriginalText) {
|
||||
this.textArea.style["background-color"] = "#faa";
|
||||
setTimeout(() => { // yay for lexical 'this'!
|
||||
this.textArea.style["background-color"] = "";
|
||||
}, 150);
|
||||
|
||||
if (!this.opts.allowLooping) {
|
||||
this.stopTabCompleting();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.textArea.style["background-color"] = ""; // cancel blinks TODO: required?
|
||||
}
|
||||
}
|
||||
|
||||
_replaceWith(newVal, includeSuffix, suffix) {
|
||||
// The regex to replace the input matches a character of whitespace AND
|
||||
// the partial word. If we just use string.replace() with the regex it will
|
||||
// replace the partial word AND the character of whitespace. We want to
|
||||
// preserve whatever that character is (\n, \t, etc) so find out what it is now.
|
||||
var boundaryChar;
|
||||
var res = MATCH_REGEX.exec(this.originalText);
|
||||
if (res) {
|
||||
boundaryChar = res[1]; // the first captured group
|
||||
}
|
||||
if (boundaryChar === undefined) {
|
||||
console.warn("Failed to find boundary char on text: '%s'", this.originalText);
|
||||
boundaryChar = "";
|
||||
}
|
||||
|
||||
suffix = suffix || "";
|
||||
if (!includeSuffix) {
|
||||
suffix = "";
|
||||
}
|
||||
|
||||
var replacementText = boundaryChar + newVal + suffix;
|
||||
return this.originalText.replace(MATCH_REGEX, function() {
|
||||
return replacementText; // function form to avoid `$` special-casing
|
||||
});
|
||||
}
|
||||
|
||||
_notifyStateChange() {
|
||||
if (this.opts.onStateChange) {
|
||||
this.opts.onStateChange(this.completing);
|
||||
}
|
||||
}
|
||||
|
||||
_sortEntries() {
|
||||
// largest comes first
|
||||
const KIND_ORDER = {
|
||||
command: 1,
|
||||
member: 2,
|
||||
};
|
||||
|
||||
this.list.sort((a, b) => {
|
||||
const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind];
|
||||
if (kindOrderDifference != 0) {
|
||||
return kindOrderDifference;
|
||||
}
|
||||
|
||||
if (a.kind == 'member') {
|
||||
let orderA = this.memberTabOrder[a.member.userId];
|
||||
let orderB = this.memberTabOrder[b.member.userId];
|
||||
if (orderA === undefined) orderA = -1;
|
||||
if (orderB === undefined) orderB = -1;
|
||||
|
||||
return orderB - orderA;
|
||||
}
|
||||
|
||||
// anything else we have no ordering for
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
_makeEntries(room) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
const members = room.getJoinedMembers().filter(function(member) {
|
||||
if (member.userId !== myUserId) return true;
|
||||
});
|
||||
|
||||
this.list = MemberEntry.fromMemberList(members).concat(
|
||||
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
||||
);
|
||||
}
|
||||
|
||||
_initSorting(room) {
|
||||
this.memberTabOrder = {};
|
||||
this.memberOrderSeq = 0;
|
||||
|
||||
for (const ev of room.getLiveTimeline().getEvents()) {
|
||||
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TabComplete;
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var sdk = require("./index");
|
||||
|
||||
class Entry {
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The text to display in this entry.
|
||||
*/
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The text to insert into the input box. Most of the time
|
||||
* this is the same as getText().
|
||||
*/
|
||||
getFillText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ReactClass} Raw JSX
|
||||
*/
|
||||
getImageJsx() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?string} The unique key= prop for React dedupe
|
||||
*/
|
||||
getKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?string} The suffix to append to the tab-complete, or null to
|
||||
* not do this.
|
||||
*/
|
||||
getSuffix(isFirstWord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this entry is clicked.
|
||||
*/
|
||||
onClick() {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
||||
class CommandEntry extends Entry {
|
||||
constructor(cmd, cmdWithArgs) {
|
||||
super(cmdWithArgs);
|
||||
this.kind = 'command';
|
||||
this.cmd = cmd;
|
||||
}
|
||||
|
||||
getFillText() {
|
||||
return this.cmd;
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.getFillText();
|
||||
}
|
||||
|
||||
getSuffix(isFirstWord) {
|
||||
return " "; // force a space after the command.
|
||||
}
|
||||
}
|
||||
|
||||
CommandEntry.fromCommands = function(commandArray) {
|
||||
return commandArray.map(function(cmd) {
|
||||
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
|
||||
});
|
||||
};
|
||||
|
||||
class MemberEntry extends Entry {
|
||||
constructor(member) {
|
||||
super((member.name || member.userId).replace(' (IRC)', ''));
|
||||
this.member = member;
|
||||
this.kind = 'member';
|
||||
}
|
||||
|
||||
getImageJsx() {
|
||||
var MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
return (
|
||||
<MemberAvatar member={this.member} width={24} height={24} />
|
||||
);
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.member.userId;
|
||||
}
|
||||
|
||||
getSuffix(isFirstWord) {
|
||||
return isFirstWord ? ": " : " ";
|
||||
}
|
||||
}
|
||||
|
||||
MemberEntry.fromMemberList = function(members) {
|
||||
return members.map(function(m) {
|
||||
return new MemberEntry(m);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.Entry = Entry;
|
||||
module.exports.MemberEntry = MemberEntry;
|
||||
module.exports.CommandEntry = CommandEntry;
|
@ -37,7 +37,26 @@ module.exports = {
|
||||
},
|
||||
|
||||
doesRoomHaveUnreadMessages: function(room) {
|
||||
var readUpToId = room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get the most recent read receipt sent by our account.
|
||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||
// despite the name of the method :((
|
||||
var readUpToId = room.getEventReadUpTo(myUserId);
|
||||
|
||||
// as we don't send RRs for our own messages, make sure we special case that
|
||||
// if *we* sent the last message into the room, we consider it not unread!
|
||||
// Should fix: https://github.com/vector-im/riot-web/issues/3263
|
||||
// https://github.com/vector-im/riot-web/issues/2427
|
||||
// ...and possibly some of the others at
|
||||
// https://github.com/vector-im/riot-web/issues/3363
|
||||
if (room.timeline.length &&
|
||||
room.timeline[room.timeline.length - 1].sender &&
|
||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// this just looks at whatever history we have, which if we've only just started
|
||||
// up probably won't be very much, so if the last couple of events are ones that
|
||||
// don't count, we don't know if there are any events that do count between where
|
||||
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var dis = require("./dispatcher");
|
||||
import dis from './dispatcher';
|
||||
|
||||
var MIN_DISPATCH_INTERVAL_MS = 500;
|
||||
var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
||||
const MIN_DISPATCH_INTERVAL_MS = 500;
|
||||
const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
||||
|
||||
/**
|
||||
* This class watches for user activity (moving the mouse or pressing a key)
|
||||
@ -58,16 +58,15 @@ class UserActivity {
|
||||
/**
|
||||
* Return true if there has been user activity very recently
|
||||
* (ie. within a few seconds)
|
||||
* @returns {boolean} true if user is currently/very recently active
|
||||
*/
|
||||
userCurrentlyActive() {
|
||||
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
|
||||
}
|
||||
|
||||
_onUserActivity(event) {
|
||||
if (event.screenX && event.type == "mousemove") {
|
||||
if (event.screenX === this.lastScreenX &&
|
||||
event.screenY === this.lastScreenY)
|
||||
{
|
||||
if (event.screenX && event.type === "mousemove") {
|
||||
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
||||
// mouse hasn't actually moved
|
||||
return;
|
||||
}
|
||||
@ -79,28 +78,24 @@ class UserActivity {
|
||||
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
||||
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||
dis.dispatch({
|
||||
action: 'user_activity'
|
||||
action: 'user_activity',
|
||||
});
|
||||
if (!this.activityEndTimer) {
|
||||
this.activityEndTimer = setTimeout(
|
||||
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
|
||||
);
|
||||
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onActivityEndTimer() {
|
||||
var now = new Date().getTime();
|
||||
var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
||||
const now = new Date().getTime();
|
||||
const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
||||
if (now >= targetTime) {
|
||||
dis.dispatch({
|
||||
action: 'user_activity_end'
|
||||
action: 'user_activity_end',
|
||||
});
|
||||
this.activityEndTimer = undefined;
|
||||
} else {
|
||||
this.activityEndTimer = setTimeout(
|
||||
this._onActivityEndTimer.bind(this), targetTime - now
|
||||
);
|
||||
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import Notifier from './Notifier';
|
||||
import { _t } from './languageHandler';
|
||||
@ -27,14 +27,14 @@ export default {
|
||||
LABS_FEATURES: [
|
||||
{
|
||||
name: "-",
|
||||
id: 'rich_text_editor',
|
||||
id: 'matrix_apps',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
|
||||
// horrible but it works. The locality makes this somewhat more palatable.
|
||||
doTranslations: function() {
|
||||
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
|
||||
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
||||
},
|
||||
|
||||
loadProfileInfo: function() {
|
||||
@ -48,7 +48,7 @@ export default {
|
||||
|
||||
loadThreePids: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return q({
|
||||
return Promise.resolve({
|
||||
threepids: [],
|
||||
}); // guests can't poke 3pid endpoint
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ module.exports = React.createClass({
|
||||
});
|
||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||
if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||
oldNode.style.visibility = c.props.style.visibility;
|
||||
}
|
||||
self.children[c.key] = old;
|
||||
|
@ -28,23 +28,31 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return { device: this.refreshDevice() };
|
||||
return { device: null };
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
var client = MatrixClientPeg.get();
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
|
||||
// no need to redownload keys if we already have the device
|
||||
if (this.state.device) {
|
||||
return;
|
||||
}
|
||||
client.downloadKeys([this.props.event.getSender()], true).done(()=>{
|
||||
// first try to load the device from our store.
|
||||
//
|
||||
this.refreshDevice().then((dev) => {
|
||||
if (dev) {
|
||||
return dev;
|
||||
}
|
||||
|
||||
// tell the client to try to refresh the device list for this user
|
||||
return client.downloadKeys([this.props.event.getSender()], true).then(() => {
|
||||
return this.refreshDevice();
|
||||
});
|
||||
}).then((dev) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({ device: this.refreshDevice() });
|
||||
|
||||
this.setState({ device: dev });
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}, (err)=>{
|
||||
console.log("Error downloading devices", err);
|
||||
});
|
||||
@ -59,12 +67,16 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
refreshDevice: function() {
|
||||
return MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event);
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event));
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.event.getSender()) {
|
||||
this.setState({ device: this.refreshDevice() });
|
||||
this.refreshDevice().then((dev) => {
|
||||
this.setState({ device: dev });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
|
||||
export default class AutocompleteProvider {
|
||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||
constructor(commandRegex?: RegExp) {
|
||||
if (commandRegex) {
|
||||
if (!commandRegex.global) {
|
||||
throw new Error('commandRegex must have global flag set');
|
||||
|
@ -22,7 +22,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
import Q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
export type SelectionRange = {
|
||||
start: number,
|
||||
@ -34,6 +34,9 @@ export type Completion = {
|
||||
component: ?Component,
|
||||
range: SelectionRange,
|
||||
command: ?string,
|
||||
// If provided, apply a LINK entity to the completion with the
|
||||
// data = { url: href }.
|
||||
href: ?string,
|
||||
};
|
||||
|
||||
const PROVIDERS = [
|
||||
@ -52,28 +55,31 @@ export async function getCompletions(query: string, selection: SelectionRange, f
|
||||
otherwise, we run into a condition where new completions are displayed
|
||||
while the user is interacting with the list, which makes it difficult
|
||||
to predict whether an action will actually do what is intended
|
||||
|
||||
It ends up containing a list of Q promise states, which are objects with
|
||||
state (== "fulfilled" || "rejected") and value. */
|
||||
const completionsList = await Q.allSettled(
|
||||
PROVIDERS.map(provider => {
|
||||
return Q(provider.getCompletions(query, selection, force))
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
||||
})
|
||||
*/
|
||||
const completionsList = await Promise.all(
|
||||
// Array of inspections of promises that might timeout. Instead of allowing a
|
||||
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||
// settled, filter for the fulfilled ones
|
||||
PROVIDERS.map((provider) => {
|
||||
return provider
|
||||
.getCompletions(query, selection, force)
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||
.reflect();
|
||||
}),
|
||||
);
|
||||
|
||||
return completionsList
|
||||
.filter(completion => completion.state === "fulfilled")
|
||||
.map((completionsState, i) => {
|
||||
return {
|
||||
completions: completionsState.value,
|
||||
provider: PROVIDERS[i],
|
||||
return completionsList.filter(
|
||||
(inspection) => inspection.isFulfilled(),
|
||||
).map((completionsState, i) => {
|
||||
return {
|
||||
completions: completionsState.value(),
|
||||
provider: PROVIDERS[i],
|
||||
|
||||
/* the currently matched "command" the completer tried to complete
|
||||
* we pass this through so that Autocomplete can figure out when to
|
||||
* re-show itself once hidden.
|
||||
*/
|
||||
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
});
|
||||
/* the currently matched "command" the completer tried to complete
|
||||
* we pass this through so that Autocomplete can figure out when to
|
||||
* re-show itself once hidden.
|
||||
*/
|
||||
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -18,9 +18,10 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Fuse from 'fuse.js';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
// TODO merge this with the factory mechanics of SlashCommands?
|
||||
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
|
||||
const COMMANDS = [
|
||||
{
|
||||
@ -33,6 +34,16 @@ const COMMANDS = [
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Bans user with given id',
|
||||
},
|
||||
{
|
||||
command: '/unban',
|
||||
args: '<user-id>',
|
||||
description: 'Unbans user with given id',
|
||||
},
|
||||
{
|
||||
command: '/op',
|
||||
args: '<user-id> [<power-level>]',
|
||||
description: 'Define the power level of a user',
|
||||
},
|
||||
{
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
@ -48,6 +59,16 @@ const COMMANDS = [
|
||||
args: '<room-alias>',
|
||||
description: 'Joins room with given alias',
|
||||
},
|
||||
{
|
||||
command: '/part',
|
||||
args: '[<room-alias>]',
|
||||
description: 'Leave room',
|
||||
},
|
||||
{
|
||||
command: '/topic',
|
||||
args: '<topic>',
|
||||
description: 'Sets the room topic',
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
@ -63,6 +84,17 @@ const COMMANDS = [
|
||||
args: '<query>',
|
||||
description: 'Searches DuckDuckGo for results',
|
||||
},
|
||||
{
|
||||
command: '/tint',
|
||||
args: '<color1> [<color2>]',
|
||||
description: 'Changes colour scheme of current room',
|
||||
},
|
||||
{
|
||||
command: '/verify',
|
||||
args: '<user-id> <device-id> <device-signing-key>',
|
||||
description: 'Verifies a user, device, and pubkey tuple',
|
||||
},
|
||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||
];
|
||||
|
||||
const COMMAND_RE = /(^\/\w*)/g;
|
||||
@ -72,7 +104,7 @@ let instance = null;
|
||||
export default class CommandProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(COMMAND_RE);
|
||||
this.fuse = new Fuse(COMMANDS, {
|
||||
this.matcher = new FuzzyMatcher(COMMANDS, {
|
||||
keys: ['command', 'args', 'description'],
|
||||
});
|
||||
}
|
||||
@ -81,7 +113,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map((result) => {
|
||||
completions = this.matcher.match(command[0]).map((result) => {
|
||||
return {
|
||||
completion: result.command + ' ',
|
||||
component: (<TextualCompletion
|
||||
|
@ -18,31 +18,96 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||
import Fuse from 'fuse.js';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import sdk from '../index';
|
||||
import {PillCompletion} from './Components';
|
||||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
|
||||
const EMOJI_REGEX = /:\w*:?/g;
|
||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
const LIMIT = 20;
|
||||
const CATEGORY_ORDER = [
|
||||
'people',
|
||||
'food',
|
||||
'objects',
|
||||
'activity',
|
||||
'nature',
|
||||
'travel',
|
||||
'flags',
|
||||
'regional',
|
||||
'symbols',
|
||||
'modifier',
|
||||
];
|
||||
|
||||
// Match for ":wink:" or ascii-style ";-)" provided by emojione
|
||||
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
|
||||
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
|
||||
// that we need to support inputting multiple emoji with no space between them.
|
||||
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:\\w*:?)$', 'g');
|
||||
|
||||
// We also need to match the non-zero-length prefixes to remove them from the final match,
|
||||
// and update the range so that we don't replace the whitespace or the previous emoji.
|
||||
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
|
||||
|
||||
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||
(a, b) => {
|
||||
if (a.category === b.category) {
|
||||
return a.emoji_order - b.emoji_order;
|
||||
}
|
||||
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
||||
},
|
||||
).map((a, index) => {
|
||||
return {
|
||||
name: a.name,
|
||||
shortname: a.shortname,
|
||||
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
|
||||
// Include the index so that we can preserve the original order
|
||||
_orderBy: index,
|
||||
};
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class EmojiProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.fuse = new Fuse(EMOJI_SHORTNAMES, {});
|
||||
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
|
||||
keys: ['aliases_ascii', 'shortname'],
|
||||
// For matching against ascii equivalents
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
this.nameMatcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
|
||||
keys: ['name'],
|
||||
// For removing punctuation
|
||||
shouldMatchWordsOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map(result => {
|
||||
const shortname = EMOJI_SHORTNAMES[result];
|
||||
let matchedString = command[0];
|
||||
|
||||
// Remove prefix of any length (single whitespace or unicode emoji)
|
||||
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
|
||||
if (prefixMatch) {
|
||||
matchedString = matchedString.slice(prefixMatch[0].length);
|
||||
range.start += prefixMatch[0].length;
|
||||
}
|
||||
completions = this.matcher.match(matchedString);
|
||||
|
||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||
// Reinstate original order
|
||||
completions = _sortBy(_uniq(completions), '_orderBy');
|
||||
completions = completions.map((result) => {
|
||||
const {shortname} = result;
|
||||
const unicode = shortnameToUnicode(shortname);
|
||||
return {
|
||||
completion: unicode,
|
||||
@ -51,7 +116,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 8);
|
||||
}).slice(0, LIMIT);
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
107
src/autocomplete/FuzzyMatcher.js
Normal file
107
src/autocomplete/FuzzyMatcher.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//import Levenshtein from 'liblevenshtein';
|
||||
//import _at from 'lodash/at';
|
||||
//import _flatMap from 'lodash/flatMap';
|
||||
//import _sortBy from 'lodash/sortBy';
|
||||
//import _sortedUniq from 'lodash/sortedUniq';
|
||||
//import _keys from 'lodash/keys';
|
||||
//
|
||||
//class KeyMap {
|
||||
// keys: Array<String>;
|
||||
// objectMap: {[String]: Array<Object>};
|
||||
// priorityMap: {[String]: number}
|
||||
//}
|
||||
//
|
||||
//const DEFAULT_RESULT_COUNT = 10;
|
||||
//const DEFAULT_DISTANCE = 5;
|
||||
|
||||
// FIXME Until Fuzzy matching works better, we use prefix matching.
|
||||
|
||||
import PrefixMatcher from './QueryMatcher';
|
||||
export default PrefixMatcher;
|
||||
|
||||
//class FuzzyMatcher { // eslint-disable-line no-unused-vars
|
||||
// /**
|
||||
// * @param {object[]} objects the objects to perform a match on
|
||||
// * @param {string[]} keys an array of keys within each object to match on
|
||||
// * Keys can refer to object properties by name and as in JavaScript (for nested properties)
|
||||
// *
|
||||
// * To use, simply presort objects by required criteria, run through this function and create a FuzzyMatcher with the
|
||||
// * resulting KeyMap.
|
||||
// *
|
||||
// * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
|
||||
// * @return {KeyMap}
|
||||
// */
|
||||
// static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
|
||||
// const keyMap = new KeyMap();
|
||||
// const map = {};
|
||||
// const priorities = {};
|
||||
//
|
||||
// objects.forEach((object, i) => {
|
||||
// const keyValues = _at(object, keys);
|
||||
// console.log(object, keyValues, keys);
|
||||
// for (const keyValue of keyValues) {
|
||||
// if (!map.hasOwnProperty(keyValue)) {
|
||||
// map[keyValue] = [];
|
||||
// }
|
||||
// map[keyValue].push(object);
|
||||
// }
|
||||
// priorities[object] = i;
|
||||
// });
|
||||
//
|
||||
// keyMap.objectMap = map;
|
||||
// keyMap.priorityMap = priorities;
|
||||
// keyMap.keys = _sortBy(_keys(map), [(value) => priorities[value]]);
|
||||
// return keyMap;
|
||||
// }
|
||||
//
|
||||
// constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
|
||||
// this.options = options;
|
||||
// this.keys = options.keys;
|
||||
// this.setObjects(objects);
|
||||
// }
|
||||
//
|
||||
// setObjects(objects: Array<Object>) {
|
||||
// this.keyMap = FuzzyMatcher.valuesToKeyMap(objects, this.keys);
|
||||
// console.log(this.keyMap.keys);
|
||||
// this.matcher = new Levenshtein.Builder()
|
||||
// .dictionary(this.keyMap.keys, true)
|
||||
// .algorithm('transposition')
|
||||
// .sort_candidates(false)
|
||||
// .case_insensitive_sort(true)
|
||||
// .include_distance(true)
|
||||
// .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense
|
||||
// .build();
|
||||
// }
|
||||
//
|
||||
// match(query: String): Array<Object> {
|
||||
// const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE);
|
||||
// // TODO FIXME This is hideous. Clean up when possible.
|
||||
// const val = _sortedUniq(_sortBy(_flatMap(candidates, (candidate) => {
|
||||
// return this.keyMap.objectMap[candidate[0]].map((value) => {
|
||||
// return {
|
||||
// distance: candidate[1],
|
||||
// ...value,
|
||||
// };
|
||||
// });
|
||||
// }),
|
||||
// [(candidate) => candidate.distance, (candidate) => this.keyMap.priorityMap[candidate]]));
|
||||
// console.log(val);
|
||||
// return val;
|
||||
// }
|
||||
//}
|
112
src/autocomplete/QueryMatcher.js
Normal file
112
src/autocomplete/QueryMatcher.js
Normal file
@ -0,0 +1,112 @@
|
||||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _at from 'lodash/at';
|
||||
import _flatMap from 'lodash/flatMap';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _keys from 'lodash/keys';
|
||||
|
||||
class KeyMap {
|
||||
keys: Array<String>;
|
||||
objectMap: {[String]: Array<Object>};
|
||||
priorityMap = new Map();
|
||||
}
|
||||
|
||||
export default class QueryMatcher {
|
||||
/**
|
||||
* @param {object[]} objects the objects to perform a match on
|
||||
* @param {string[]} keys an array of keys within each object to match on
|
||||
* Keys can refer to object properties by name and as in JavaScript (for nested properties)
|
||||
*
|
||||
* To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the
|
||||
* resulting KeyMap.
|
||||
*
|
||||
* TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
|
||||
* @return {KeyMap}
|
||||
*/
|
||||
static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
|
||||
const keyMap = new KeyMap();
|
||||
const map = {};
|
||||
|
||||
objects.forEach((object, i) => {
|
||||
const keyValues = _at(object, keys);
|
||||
for (const keyValue of keyValues) {
|
||||
if (!map.hasOwnProperty(keyValue)) {
|
||||
map[keyValue] = [];
|
||||
}
|
||||
map[keyValue].push(object);
|
||||
}
|
||||
keyMap.priorityMap.set(object, i);
|
||||
});
|
||||
|
||||
keyMap.objectMap = map;
|
||||
keyMap.keys = _keys(map);
|
||||
return keyMap;
|
||||
}
|
||||
|
||||
constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
|
||||
this.options = options;
|
||||
this.keys = options.keys;
|
||||
this.setObjects(objects);
|
||||
|
||||
// By default, we remove any non-alphanumeric characters ([^A-Za-z0-9_]) from the
|
||||
// query and the value being queried before matching
|
||||
if (this.options.shouldMatchWordsOnly === undefined) {
|
||||
this.options.shouldMatchWordsOnly = true;
|
||||
}
|
||||
|
||||
// By default, match anywhere in the string being searched. If enabled, only return
|
||||
// matches that are prefixed with the query.
|
||||
if (this.options.shouldMatchPrefix === undefined) {
|
||||
this.options.shouldMatchPrefix = false;
|
||||
}
|
||||
}
|
||||
|
||||
setObjects(objects: Array<Object>) {
|
||||
this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys);
|
||||
}
|
||||
|
||||
match(query: String): Array<Object> {
|
||||
query = query.toLowerCase();
|
||||
if (this.options.shouldMatchWordsOnly) {
|
||||
query = query.replace(/[^\w]/g, '');
|
||||
}
|
||||
if (query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
this.keyMap.keys.forEach((key) => {
|
||||
let resultKey = key.toLowerCase();
|
||||
if (this.options.shouldMatchWordsOnly) {
|
||||
resultKey = resultKey.replace(/[^\w]/g, '');
|
||||
}
|
||||
const index = resultKey.indexOf(query);
|
||||
if (index !== -1 && (!this.options.shouldMatchPrefix || index === 0)) {
|
||||
results.push({key, index});
|
||||
}
|
||||
});
|
||||
|
||||
return _uniq(_flatMap(_sortBy(results, (candidate) => {
|
||||
return candidate.index;
|
||||
}).map((candidate) => {
|
||||
// return an array of objects (those given to setObjects) that have the given
|
||||
// key as a property.
|
||||
return this.keyMap.objectMap[candidate.key];
|
||||
})));
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import Fuse from 'fuse.js';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {getDisplayAliasForRoom} from '../Rooms';
|
||||
import sdk from '../index';
|
||||
@ -30,11 +30,9 @@ let instance = null;
|
||||
|
||||
export default class RoomProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(ROOM_REGEX, {
|
||||
keys: ['displayName', 'userId'],
|
||||
});
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['name', 'roomId', 'aliases'],
|
||||
super(ROOM_REGEX);
|
||||
this.matcher = new FuzzyMatcher([], {
|
||||
keys: ['name', 'roomId', 'aliases'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -46,17 +44,19 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
|
||||
this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => {
|
||||
return {
|
||||
room: room,
|
||||
name: room.name,
|
||||
aliases: room.getAliases(),
|
||||
};
|
||||
}));
|
||||
completions = this.fuse.search(command[0]).map(room => {
|
||||
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||
completions = this.matcher.match(command[0]).map(room => {
|
||||
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||
return {
|
||||
completion: displayAlias,
|
||||
suffix: ' ',
|
||||
href: 'https://matrix.to/#/' + displayAlias,
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||
),
|
||||
@ -80,12 +80,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{completions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
shouldForceComplete(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
//@flow
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
@ -18,22 +19,29 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Fuse from 'fuse.js';
|
||||
import {PillCompletion} from './Components';
|
||||
import sdk from '../index';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import _pull from 'lodash/pull';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
|
||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||
|
||||
const USER_REGEX = /@\S*/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class UserProvider extends AutocompleteProvider {
|
||||
users: Array<RoomMember> = [];
|
||||
|
||||
constructor() {
|
||||
super(USER_REGEX, {
|
||||
keys: ['name', 'userId'],
|
||||
keys: ['name'],
|
||||
});
|
||||
this.users = [];
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['name', 'userId'],
|
||||
this.matcher = new FuzzyMatcher([], {
|
||||
keys: ['name'],
|
||||
shouldMatchPrefix: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -43,17 +51,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
this.fuse.set(this.users);
|
||||
completions = this.fuse.search(command[0]).map(user => {
|
||||
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||
let completion = displayName;
|
||||
if (range.start === 0) {
|
||||
completion += ': ';
|
||||
} else {
|
||||
completion += ' ';
|
||||
}
|
||||
completions = this.matcher.match(command[0]).map((user) => {
|
||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||
return {
|
||||
completion,
|
||||
completion: displayName,
|
||||
suffix: range.start === 0 ? ': ' : ' ',
|
||||
href: 'https://matrix.to/#/' + user.userId,
|
||||
component: (
|
||||
<PillCompletion
|
||||
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||
@ -62,7 +65,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
@ -71,8 +74,35 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
return '👥 ' + _t('Users');
|
||||
}
|
||||
|
||||
setUserList(users) {
|
||||
this.users = users;
|
||||
setUserListFromRoom(room: Room) {
|
||||
const events = room.getLiveTimeline().getEvents();
|
||||
const lastSpoken = {};
|
||||
|
||||
for(const event of events) {
|
||||
lastSpoken[event.getSender()] = event.getTs();
|
||||
}
|
||||
|
||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||
this.users = room.getJoinedMembers().filter((member) => {
|
||||
if (member.userId !== currentUserId) return true;
|
||||
});
|
||||
|
||||
this.users = _sortBy(this.users, (member) =>
|
||||
1E20 - lastSpoken[member.userId] || 1E20,
|
||||
);
|
||||
|
||||
this.matcher.setObjects(this.users);
|
||||
}
|
||||
|
||||
onUserSpoke(user: RoomMember) {
|
||||
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||
|
||||
// Move the user that spoke to the front of the array
|
||||
this.users.splice(
|
||||
this.users.findIndex((user2) => user2.userId === user.userId), 1);
|
||||
this.users = [user, ...this.users];
|
||||
|
||||
this.matcher.setObjects(this.users);
|
||||
}
|
||||
|
||||
static getInstance(): UserProvider {
|
||||
@ -83,7 +113,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{completions}
|
||||
</div>;
|
||||
}
|
||||
|
382
src/components/structures/GroupView.js
Normal file
382
src/components/structures/GroupView.js
Normal file
@ -0,0 +1,382 @@
|
||||
/*
|
||||
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 MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||
import { _t } from '../../languageHandler';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
const RoomSummaryType = PropTypes.shape({
|
||||
room_id: PropTypes.string.isRequired,
|
||||
profile: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
avatar_url: PropTypes.string,
|
||||
canonical_alias: PropTypes.string,
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
const UserSummaryType = PropTypes.shape({
|
||||
summaryInfo: PropTypes.shape({
|
||||
user_id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
const CategoryRoomList = React.createClass({
|
||||
displayName: 'CategoryRoomList',
|
||||
|
||||
props: {
|
||||
rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
|
||||
category: PropTypes.shape({
|
||||
profile: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const roomNodes = this.props.rooms.map((r) => {
|
||||
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
|
||||
});
|
||||
let catHeader = null;
|
||||
if (this.props.category && this.props.category.profile) {
|
||||
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
|
||||
}
|
||||
return <div>
|
||||
{catHeader}
|
||||
{roomNodes}
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const FeaturedRoom = React.createClass({
|
||||
displayName: 'FeaturedRoom',
|
||||
|
||||
props: {
|
||||
summaryInfo: RoomSummaryType.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_alias: this.props.summaryInfo.profile.canonical_alias,
|
||||
room_id: this.props.summaryInfo.room_id,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
const oobData = {
|
||||
roomId: this.props.summaryInfo.room_id,
|
||||
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
||||
name: this.props.summaryInfo.profile.name,
|
||||
};
|
||||
let permalink = null;
|
||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||
}
|
||||
let roomNameNode = null;
|
||||
if (permalink) {
|
||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
|
||||
} else {
|
||||
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
|
||||
}
|
||||
|
||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||
<RoomAvatar oobData={oobData} width={64} height={64} />
|
||||
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
||||
const RoleUserList = React.createClass({
|
||||
displayName: 'RoleUserList',
|
||||
|
||||
props: {
|
||||
users: PropTypes.arrayOf(UserSummaryType).isRequired,
|
||||
role: PropTypes.shape({
|
||||
profile: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const userNodes = this.props.users.map((u) => {
|
||||
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
|
||||
});
|
||||
let roleHeader = null;
|
||||
if (this.props.role && this.props.role.profile) {
|
||||
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
|
||||
}
|
||||
return <div>
|
||||
{roleHeader}
|
||||
{userNodes}
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const FeaturedUser = React.createClass({
|
||||
displayName: 'FeaturedUser',
|
||||
|
||||
props: {
|
||||
summaryInfo: UserSummaryType.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_start_chat_or_reuse',
|
||||
user_id: this.props.summaryInfo.user_id,
|
||||
go_home_on_cancel: false,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Add avatar once we get profile info inline in the summary response
|
||||
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
|
||||
|
||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupView',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
summary: null,
|
||||
error: null,
|
||||
editing: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._loadGroupFromServer(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (this.props.groupId != newProps.groupId) {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: null,
|
||||
}, () => {
|
||||
this._loadGroupFromServer(newProps.groupId);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_loadGroupFromServer: function(groupId) {
|
||||
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
|
||||
this.setState({
|
||||
summary: res,
|
||||
error: null,
|
||||
});
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onSettingsClick: function() {
|
||||
this.setState({editing: true});
|
||||
},
|
||||
|
||||
_getFeaturedRoomsNode() {
|
||||
const summary = this.state.summary;
|
||||
|
||||
if (summary.rooms_section.rooms.length == 0) return null;
|
||||
|
||||
const defaultCategoryRooms = [];
|
||||
const categoryRooms = {};
|
||||
summary.rooms_section.rooms.forEach((r) => {
|
||||
if (r.category_id === null) {
|
||||
defaultCategoryRooms.push(r);
|
||||
} else {
|
||||
let list = categoryRooms[r.category_id];
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
categoryRooms[r.category_id] = list;
|
||||
}
|
||||
list.push(r);
|
||||
}
|
||||
});
|
||||
|
||||
let defaultCategoryNode = null;
|
||||
if (defaultCategoryRooms.length > 0) {
|
||||
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
|
||||
}
|
||||
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
||||
const cat = summary.rooms_section.categories[catId];
|
||||
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
|
||||
});
|
||||
|
||||
return <div className="mx_GroupView_featuredThings">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Rooms:')}
|
||||
</div>
|
||||
{defaultCategoryNode}
|
||||
{categoryRoomNodes}
|
||||
</div>;
|
||||
},
|
||||
|
||||
_getFeaturedUsersNode() {
|
||||
const summary = this.state.summary;
|
||||
|
||||
if (summary.users_section.users.length == 0) return null;
|
||||
|
||||
const noRoleUsers = [];
|
||||
const roleUsers = {};
|
||||
summary.users_section.users.forEach((u) => {
|
||||
if (u.role_id === null) {
|
||||
noRoleUsers.push(u);
|
||||
} else {
|
||||
let list = roleUsers[u.role_id];
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
roleUsers[u.role_id] = list;
|
||||
}
|
||||
list.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
let noRoleNode = null;
|
||||
if (noRoleUsers.length > 0) {
|
||||
noRoleNode = <RoleUserList users={noRoleUsers} />;
|
||||
}
|
||||
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
||||
const role = summary.users_section.roles[roleId];
|
||||
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
|
||||
});
|
||||
|
||||
return <div className="mx_GroupView_featuredThings">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Users:')}
|
||||
</div>
|
||||
{noRoleNode}
|
||||
{roleUserNodes}
|
||||
</div>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
if (this.state.summary === null && this.state.error === null) {
|
||||
return <Loader />;
|
||||
} else if (this.state.editing) {
|
||||
return <div />;
|
||||
} else if (this.state.summary) {
|
||||
const summary = this.state.summary;
|
||||
let description = null;
|
||||
if (summary.profile && summary.profile.long_description) {
|
||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||
}
|
||||
|
||||
const roomBody = <div>
|
||||
<div className="mx_GroupView_groupDesc">{description}</div>
|
||||
{this._getFeaturedRoomsNode()}
|
||||
{this._getFeaturedUsersNode()}
|
||||
</div>;
|
||||
|
||||
let nameNode;
|
||||
if (summary.profile && summary.profile.name) {
|
||||
nameNode = <div className="mx_RoomHeader_name">
|
||||
<span>{summary.profile.name}</span>
|
||||
<span className="mx_GroupView_header_groupid">
|
||||
({this.props.groupId})
|
||||
</span>
|
||||
</div>;
|
||||
} else {
|
||||
nameNode = <div className="mx_RoomHeader_name">
|
||||
<span>{this.props.groupId}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
|
||||
// settings button is display: none until settings is wired up
|
||||
return (
|
||||
<div className="mx_GroupView">
|
||||
<div className="mx_RoomHeader">
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">
|
||||
<GroupAvatar
|
||||
groupId={this.props.groupId}
|
||||
groupAvatarUrl={groupAvatarUrl}
|
||||
width={48} height={48}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_RoomHeader_info">
|
||||
{nameNode}
|
||||
<div className="mx_RoomHeader_topic">
|
||||
{summary.profile.short_description}
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this._onSettingsClick} title={_t("Settings")} style={{display: 'none'}}>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
{roomBody}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
if (this.state.error.httpStatus === 404) {
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
Group {this.props.groupId} not found
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let extraText;
|
||||
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
||||
extraText = <div>{_t('This Home server does not support groups')}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
Failed to load {this.props.groupId}
|
||||
{extraText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid state for GroupView");
|
||||
return <div />;
|
||||
}
|
||||
},
|
||||
});
|
@ -156,13 +156,20 @@ export default React.createClass({
|
||||
}
|
||||
*/
|
||||
|
||||
var handled = false;
|
||||
let handled = false;
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
let ctrlCmdOnly;
|
||||
if (isMac) {
|
||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||
} else {
|
||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
var action = ev.keyCode == KeyCode.UP ?
|
||||
let action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
handled = true;
|
||||
@ -184,6 +191,14 @@ export default React.createClass({
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.KEY_K:
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'focus_room_filter',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
@ -210,8 +225,11 @@ export default React.createClass({
|
||||
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
const HomePage = sdk.getComponent('structures.HomePage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
|
||||
let page_element;
|
||||
@ -239,7 +257,6 @@ export default React.createClass({
|
||||
page_element = <UserSettings
|
||||
onClose={this.props.onUserSettingsClose}
|
||||
brand={this.props.config.brand}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
enableLabs={this.props.config.enableLabs}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
@ -247,6 +264,10 @@ export default React.createClass({
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
page_element = <MyGroups />;
|
||||
break;
|
||||
|
||||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
onRoomCreated={this.props.onRoomCreated}
|
||||
@ -263,32 +284,40 @@ export default React.createClass({
|
||||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
// If team server config is present, pass the teamServerURL. props.teamToken
|
||||
// must also be set for the team page to be displayed, otherwise the
|
||||
// welcomePageUrl is used (which might be undefined).
|
||||
const teamServerUrl = this.props.config.teamServerConfig ?
|
||||
this.props.config.teamServerConfig.teamServerURL : null;
|
||||
{
|
||||
// If team server config is present, pass the teamServerURL. props.teamToken
|
||||
// must also be set for the team page to be displayed, otherwise the
|
||||
// welcomePageUrl is used (which might be undefined).
|
||||
const teamServerUrl = this.props.config.teamServerConfig ?
|
||||
this.props.config.teamServerConfig.teamServerURL : null;
|
||||
|
||||
page_element = <HomePage
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
teamServerUrl={teamServerUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
homePageUrl={this.props.config.welcomePageUrl}
|
||||
/>;
|
||||
page_element = <HomePage
|
||||
teamServerUrl={teamServerUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
homePageUrl={this.props.config.welcomePageUrl}
|
||||
/>;
|
||||
}
|
||||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
page_element = null; // deliberately null for now
|
||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
||||
break;
|
||||
case PageTypes.GroupView:
|
||||
page_element = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
let topBar;
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
var topBar;
|
||||
if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
} else if (this.props.checkingForUpdate) {
|
||||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import React from 'react';
|
||||
import Matrix from "matrix-js-sdk";
|
||||
@ -41,9 +41,44 @@ import PageTypes from '../../PageTypes';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
const VIEWS = {
|
||||
// a special initial state which is only used at startup, while we are
|
||||
// trying to re-animate a matrix client or register as a guest.
|
||||
LOADING: 0,
|
||||
|
||||
// we are showing the login view
|
||||
LOGIN: 1,
|
||||
|
||||
// we are showing the registration view
|
||||
REGISTER: 2,
|
||||
|
||||
// completeing the registration flow
|
||||
POST_REGISTRATION: 3,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD: 4,
|
||||
|
||||
// we have valid matrix credentials (either via an explicit login, via the
|
||||
// initial re-animation/guest registration, or via a registration), and are
|
||||
// now setting up a matrixclient to talk to it. This isn't an instant
|
||||
// process because (a) we need to clear out indexeddb, and (b) we need to
|
||||
// talk to the team server; while it is going on we show a big spinner.
|
||||
LOGGING_IN: 5,
|
||||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN: 6,
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
// we export this so that the integration tests can use it :-S
|
||||
statics: {
|
||||
VIEWS: VIEWS,
|
||||
},
|
||||
|
||||
displayName: 'MatrixChat',
|
||||
|
||||
propTypes: {
|
||||
@ -59,8 +94,8 @@ module.exports = React.createClass({
|
||||
// the initial queryParams extracted from the hash-fragment of the URI
|
||||
startingFragmentQueryParams: React.PropTypes.object,
|
||||
|
||||
// called when the session load completes
|
||||
onLoadCompleted: React.PropTypes.func,
|
||||
// called when we have completed a token login
|
||||
onTokenLoginCompleted: React.PropTypes.func,
|
||||
|
||||
// Represents the screen to display as a result of parsing the initial
|
||||
// window.location
|
||||
@ -93,14 +128,11 @@ module.exports = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
const s = {
|
||||
loading: true,
|
||||
screen: undefined,
|
||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
||||
// the master view we are showing.
|
||||
view: VIEWS.LOADING,
|
||||
|
||||
// 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,
|
||||
// a thing to call showScreen with once login completes.
|
||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
||||
|
||||
// What the LoggedInView would be showing if visible
|
||||
page_type: null,
|
||||
@ -113,8 +145,6 @@ module.exports = React.createClass({
|
||||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||
viewUserId: null,
|
||||
|
||||
loggedIn: false,
|
||||
loggingIn: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
ready: false,
|
||||
@ -127,6 +157,7 @@ module.exports = React.createClass({
|
||||
newVersion: null,
|
||||
hasNewVersion: false,
|
||||
newVersionReleaseNotes: null,
|
||||
checkingForUpdate: null,
|
||||
|
||||
// Parameters used in the registration dance with the IS
|
||||
register_client_secret: null,
|
||||
@ -143,7 +174,7 @@ module.exports = React.createClass({
|
||||
realQueryParams: {},
|
||||
startingFragmentQueryParams: {},
|
||||
config: {},
|
||||
onLoadCompleted: () => {},
|
||||
onTokenLoginCompleted: () => {},
|
||||
};
|
||||
},
|
||||
|
||||
@ -193,7 +224,7 @@ module.exports = React.createClass({
|
||||
|
||||
// Used by _viewRoom before getting state from sync
|
||||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = q.defer();
|
||||
this.firstSyncPromise = Promise.defer();
|
||||
|
||||
if (this.props.config.sync_timeline_limit) {
|
||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
@ -259,6 +290,9 @@ module.exports = React.createClass({
|
||||
if (this.onUserClick) {
|
||||
linkifyMatrix.onUserClick = this.onUserClick;
|
||||
}
|
||||
if (this.onGroupClick) {
|
||||
linkifyMatrix.onGroupClick = this.onGroupClick;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
@ -266,39 +300,49 @@ module.exports = React.createClass({
|
||||
const teamServerConfig = this.props.config.teamServerConfig || {};
|
||||
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
|
||||
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
// the old creds, but rather go straight to the relevant page
|
||||
// the first thing to do is to try the token params in the query-string
|
||||
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
|
||||
if(loggedIn) {
|
||||
this.props.onTokenLoginCompleted();
|
||||
|
||||
const firstScreen = this.state.screenAfterLogin ?
|
||||
this.state.screenAfterLogin.screen : null;
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
// the 'loading' state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstScreen === 'login' ||
|
||||
firstScreen === 'register' ||
|
||||
firstScreen === 'forgot_password') {
|
||||
this.props.onLoadCompleted();
|
||||
this.setState({loading: false});
|
||||
this._showScreenAfterLogin();
|
||||
return;
|
||||
}
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
// the old creds, but rather go straight to the relevant page
|
||||
const firstScreen = this.state.screenAfterLogin ?
|
||||
this.state.screenAfterLogin.screen : null;
|
||||
|
||||
// the extra q() ensures that synchronous exceptions hit the same codepath as
|
||||
// asynchronous ones.
|
||||
q().then(() => {
|
||||
return Lifecycle.loadSession({
|
||||
realQueryParams: this.props.realQueryParams,
|
||||
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
||||
enableGuest: this.props.enableGuest,
|
||||
guestHsUrl: this.getCurrentHsUrl(),
|
||||
guestIsUrl: this.getCurrentIsUrl(),
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
if (firstScreen === 'login' ||
|
||||
firstScreen === 'register' ||
|
||||
firstScreen === 'forgot_password') {
|
||||
this.setState({loading: false});
|
||||
this._showScreenAfterLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// the extra Promise.resolve() ensures that synchronous exceptions hit the same codepath as
|
||||
// asynchronous ones.
|
||||
return Promise.resolve().then(() => {
|
||||
return Lifecycle.loadSession({
|
||||
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
||||
enableGuest: this.props.enableGuest,
|
||||
guestHsUrl: this.getCurrentHsUrl(),
|
||||
guestIsUrl: this.getCurrentIsUrl(),
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error(`Error attempting to load session: ${e}`);
|
||||
return false;
|
||||
}).then((loadedSession) => {
|
||||
if (!loadedSession) {
|
||||
// fall back to showing the login screen
|
||||
dis.dispatch({action: "start_login"});
|
||||
}
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error("Unable to load session", e);
|
||||
}).done(()=>{
|
||||
// stuff this through the dispatcher so that it happens
|
||||
// after the on_logged_in action.
|
||||
dis.dispatch({action: 'load_completed'});
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
@ -317,18 +361,19 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
setStateForNewScreen: function(state) {
|
||||
setStateForNewView: function(state) {
|
||||
if (state.view === undefined) {
|
||||
throw new Error("setStateForNewView with no view!");
|
||||
}
|
||||
const newState = {
|
||||
screen: undefined,
|
||||
viewUserId: null,
|
||||
loggedIn: false,
|
||||
ready: false,
|
||||
};
|
||||
Object.assign(newState, state);
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
@ -340,26 +385,19 @@ module.exports = React.createClass({
|
||||
this._startRegistration(payload.params || {});
|
||||
break;
|
||||
case 'start_login':
|
||||
if (MatrixClientPeg.get() &&
|
||||
MatrixClientPeg.get().isGuest()
|
||||
) {
|
||||
this.setState({
|
||||
guestCreds: MatrixClientPeg.getCredentials(),
|
||||
});
|
||||
}
|
||||
this.setStateForNewScreen({
|
||||
screen: 'login',
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGIN,
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({ // don't clobber loggedIn status
|
||||
screen: 'post_registration',
|
||||
this.setState({
|
||||
view: VIEWS.POST_REGISTRATION,
|
||||
});
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
this.setStateForNewScreen({
|
||||
screen: 'forgot_password',
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.FORGOT_PASSWORD,
|
||||
});
|
||||
this.notifyNewScreen('forgot_password');
|
||||
break;
|
||||
@ -383,7 +421,7 @@ module.exports = React.createClass({
|
||||
|
||||
MatrixClientPeg.get().leave(payload.room_id).done(() => {
|
||||
modal.close();
|
||||
if (this.currentRoomId === payload.room_id) {
|
||||
if (this.state.currentRoomId === payload.room_id) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
@ -448,6 +486,18 @@ module.exports = React.createClass({
|
||||
this._setPage(PageTypes.RoomDirectory);
|
||||
this.notifyNewScreen('directory');
|
||||
break;
|
||||
case 'view_my_groups':
|
||||
this._setPage(PageTypes.MyGroups);
|
||||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
{
|
||||
const groupId = payload.group_id;
|
||||
this.setState({currentGroupId: groupId});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
@ -456,7 +506,7 @@ module.exports = React.createClass({
|
||||
this._setMxId(payload);
|
||||
break;
|
||||
case 'view_start_chat_or_reuse':
|
||||
this._chatCreateOrReuse(payload.user_id);
|
||||
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
|
||||
break;
|
||||
case 'view_create_chat':
|
||||
this._createChat();
|
||||
@ -500,10 +550,11 @@ module.exports = React.createClass({
|
||||
break;
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// and also that we're not ready (we'll be marked as logged
|
||||
// in once the login completes, then ready once the sync
|
||||
// completes).
|
||||
this.setState({loggingIn: true, ready: false});
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
// are delayed, the sync could already have completed
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGGING_IN,
|
||||
});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
this._onLoggedIn(payload.teamToken);
|
||||
@ -512,10 +563,12 @@ module.exports = React.createClass({
|
||||
this._onLoggedOut();
|
||||
break;
|
||||
case 'will_start_client':
|
||||
this._onWillStartClient();
|
||||
break;
|
||||
case 'load_completed':
|
||||
this._onLoadCompleted();
|
||||
this.setState({ready: false}, () => {
|
||||
// if the client is about to start, we are, by definition, not ready.
|
||||
// Set ready to false now, then it'll be set to true when the sync
|
||||
// listener we set below fires.
|
||||
this._onWillStartClient();
|
||||
});
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
@ -523,6 +576,12 @@ module.exports = React.createClass({
|
||||
payload.releaseNotes,
|
||||
);
|
||||
break;
|
||||
case 'check_updates':
|
||||
this.setState({ checkingForUpdate: payload.value });
|
||||
break;
|
||||
case 'send_event':
|
||||
this.onSendEvent(payload.room_id, payload.event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@ -537,8 +596,8 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_startRegistration: function(params) {
|
||||
this.setStateForNewScreen({
|
||||
screen: 'register',
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.REGISTER,
|
||||
// these params may be undefined, but if they are,
|
||||
// unset them from our state: we don't want to
|
||||
// resume a previous registration session if the
|
||||
@ -635,7 +694,7 @@ module.exports = React.createClass({
|
||||
|
||||
// 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);
|
||||
let waitFor = Promise.resolve(null);
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
|
||||
@ -742,7 +801,9 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_chatCreateOrReuse: function(userId) {
|
||||
_chatCreateOrReuse: function(userId, goHomeOnCancel) {
|
||||
if (goHomeOnCancel === undefined) goHomeOnCancel = true;
|
||||
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
'views.dialogs.ChatCreateOrReuseDialog',
|
||||
);
|
||||
@ -773,7 +834,7 @@ module.exports = React.createClass({
|
||||
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
|
||||
userId: userId,
|
||||
onFinished: (success) => {
|
||||
if (!success) {
|
||||
if (!success && goHomeOnCancel) {
|
||||
// Dialog cancelled, default to home
|
||||
dis.dispatch({ action: 'view_home_page' });
|
||||
}
|
||||
@ -846,14 +907,6 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the sessionloader has finished
|
||||
*/
|
||||
_onLoadCompleted: function() {
|
||||
this.props.onLoadCompleted();
|
||||
this.setState({loading: false});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever someone changes the theme
|
||||
*
|
||||
@ -906,9 +959,7 @@ module.exports = React.createClass({
|
||||
*/
|
||||
_onLoggedIn: function(teamToken) {
|
||||
this.setState({
|
||||
guestCreds: null,
|
||||
loggedIn: true,
|
||||
loggingIn: false,
|
||||
view: VIEWS.LOGGED_IN,
|
||||
});
|
||||
|
||||
if (teamToken) {
|
||||
@ -917,10 +968,6 @@ module.exports = React.createClass({
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
// reset the 'have completed first sync' flag,
|
||||
// since we've just logged in and will be about to sync
|
||||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = q.defer();
|
||||
|
||||
// Set the display name = user ID localpart
|
||||
MatrixClientPeg.get().setDisplayName(
|
||||
@ -969,8 +1016,8 @@ module.exports = React.createClass({
|
||||
*/
|
||||
_onLoggedOut: function() {
|
||||
this.notifyNewScreen('login');
|
||||
this.setStateForNewScreen({
|
||||
loggedIn: false,
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
@ -978,6 +1025,7 @@ module.exports = React.createClass({
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
this._teamToken = null;
|
||||
this._setPageSubtitle();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -986,6 +1034,12 @@ module.exports = React.createClass({
|
||||
*/
|
||||
_onWillStartClient() {
|
||||
const self = this;
|
||||
|
||||
// reset the 'have completed first sync' flag,
|
||||
// since we're about to start the client and therefore about
|
||||
// to do the first sync
|
||||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = Promise.defer();
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||
@ -1056,6 +1110,14 @@ module.exports = React.createClass({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
});
|
||||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
},
|
||||
|
||||
showScreen: function(screen, params) {
|
||||
@ -1095,6 +1157,10 @@ module.exports = React.createClass({
|
||||
dis.dispatch({
|
||||
action: 'view_room_directory',
|
||||
});
|
||||
} else if (screen == 'groups') {
|
||||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
});
|
||||
} else if (screen == 'post_registration') {
|
||||
dis.dispatch({
|
||||
action: 'start_post_registration',
|
||||
@ -1133,7 +1199,7 @@ module.exports = React.createClass({
|
||||
|
||||
// we can't view a room unless we're logged in
|
||||
// (a guest account is fine)
|
||||
if (this.state.loggedIn) {
|
||||
if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
} else if (screen.indexOf('user/') == 0) {
|
||||
@ -1154,6 +1220,15 @@ module.exports = React.createClass({
|
||||
member: member,
|
||||
});
|
||||
}
|
||||
} else if (screen.indexOf('group/') == 0) {
|
||||
const groupId = screen.substring(6);
|
||||
|
||||
// TODO: Check valid group ID
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: groupId,
|
||||
});
|
||||
} else {
|
||||
console.info("Ignoring showScreen for '%s'", screen);
|
||||
}
|
||||
@ -1182,6 +1257,11 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
onGroupClick: function(event, groupId) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({action: 'view_group', group_id: groupId});
|
||||
},
|
||||
|
||||
onLogoutClick: function(event) {
|
||||
dis.dispatch({
|
||||
action: 'logout',
|
||||
@ -1231,29 +1311,25 @@ module.exports = React.createClass({
|
||||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onReturnToGuestClick: function() {
|
||||
// reanimate our guest login
|
||||
if (this.state.guestCreds) {
|
||||
// TODO: this is probably a bit broken - we don't want to be
|
||||
// clearing storage when we reanimate the guest creds.
|
||||
Lifecycle.setLoggedIn(this.state.guestCreds);
|
||||
this.setState({guestCreds: null});
|
||||
}
|
||||
onReturnToAppClick: function() {
|
||||
// treat it the same as if the user had completed the login
|
||||
this._onLoggedIn(null);
|
||||
},
|
||||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
onRegistered: function(credentials, teamToken) {
|
||||
// XXX: These both should be in state or ideally store(s) because we risk not
|
||||
// rendering the most up-to-date view of state otherwise.
|
||||
// teamToken may not be truthy
|
||||
this._teamToken = teamToken;
|
||||
this._is_registered = true;
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
},
|
||||
|
||||
onFinishPostRegistration: function() {
|
||||
// Don't confuse this with "PageType" which is the middle window to show
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
view: VIEWS.LOGGED_IN,
|
||||
});
|
||||
this.showScreen("settings");
|
||||
},
|
||||
@ -1264,9 +1340,35 @@ module.exports = React.createClass({
|
||||
newVersion: latest,
|
||||
hasNewVersion: current !== latest,
|
||||
newVersionReleaseNotes: releaseNotes,
|
||||
checkingForUpdate: null,
|
||||
});
|
||||
},
|
||||
|
||||
onSendEvent: function(roomId, event) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
dis.dispatch({action: 'message_send_failed'});
|
||||
return;
|
||||
}
|
||||
|
||||
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
|
||||
dis.dispatch({action: 'message_sent'});
|
||||
}, (err) => {
|
||||
if (err.name === 'UnknownDeviceError') {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: cli.getRoom(roomId),
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'message_send_failed'});
|
||||
});
|
||||
},
|
||||
|
||||
_setPageSubtitle: function(subtitle='') {
|
||||
document.title = `Riot ${subtitle}`;
|
||||
},
|
||||
|
||||
updateStatusIndicator: function(state, prevState) {
|
||||
let notifCount = 0;
|
||||
|
||||
@ -1287,15 +1389,15 @@ module.exports = React.createClass({
|
||||
PlatformPeg.get().setNotificationCount(notifCount);
|
||||
}
|
||||
|
||||
let title = "Riot ";
|
||||
let subtitle = '';
|
||||
if (state === "ERROR") {
|
||||
title += `[${_t("Offline")}] `;
|
||||
subtitle += `[${_t("Offline")}] `;
|
||||
}
|
||||
if (notifCount > 0) {
|
||||
title += `[${notifCount}]`;
|
||||
subtitle += `[${notifCount}]`;
|
||||
}
|
||||
|
||||
document.title = title;
|
||||
this._setPageSubtitle(subtitle);
|
||||
},
|
||||
|
||||
onUserSettingsClose: function() {
|
||||
@ -1321,11 +1423,9 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// `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) {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
@ -1335,7 +1435,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.screen == 'post_registration') {
|
||||
if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
||||
return (
|
||||
<PostRegistration
|
||||
@ -1343,38 +1443,42 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
// `ready` and `loggedIn` may be set before `page_type` (because the
|
||||
// latter is set via the dispatcher). If we don't yet have a `page_type`,
|
||||
// keep showing the spinner for now.
|
||||
if (this.state.loggedIn && this.state.ready && this.state.page_type) {
|
||||
/* 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()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
teamToken={this._teamToken}
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.loggedIn) {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
|
||||
// latter is set via the dispatcher). If we don't yet have a `page_type`,
|
||||
// keep showing the spinner for now.
|
||||
if (this.state.ready && this.state.page_type) {
|
||||
/* 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()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
teamToken={this._teamToken}
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.REGISTER) {
|
||||
const Registration = sdk.getComponent('structures.login.Registration');
|
||||
return (
|
||||
<Registration
|
||||
@ -1394,10 +1498,13 @@ module.exports = React.createClass({
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.screen == 'forgot_password') {
|
||||
}
|
||||
|
||||
|
||||
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
|
||||
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
return (
|
||||
<ForgotPassword
|
||||
@ -1409,7 +1516,9 @@ module.exports = React.createClass({
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.LOGIN) {
|
||||
const Login = sdk.getComponent('structures.login.Login');
|
||||
return (
|
||||
<Login
|
||||
@ -1423,9 +1532,11 @@ module.exports = React.createClass({
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
console.error(`Unknown view ${this.state.view}`);
|
||||
},
|
||||
});
|
||||
|
141
src/components/structures/MyGroups.js
Normal file
141
src/components/structures/MyGroups.js
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
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 { _t, _tJsx } from '../../languageHandler';
|
||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import dis from '../../dispatcher';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../Modal';
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
||||
},
|
||||
});
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'MyGroups',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
groups: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._fetch();
|
||||
},
|
||||
|
||||
_onCreateGroupClick: function() {
|
||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||
Modal.createDialog(CreateGroupDialog);
|
||||
},
|
||||
|
||||
_fetch: function() {
|
||||
this.props.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.setState({groups: result.groups, error: null});
|
||||
}, (err) => {
|
||||
this.setState({groups: null, error: err});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let content;
|
||||
if (this.state.groups) {
|
||||
const groupNodes = [];
|
||||
this.state.groups.forEach((g) => {
|
||||
groupNodes.push(
|
||||
<div key={g}>
|
||||
<GroupTile groupId={g} />
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
content = <div>
|
||||
<div>{_t('You are a member of these groups:')}</div>
|
||||
{groupNodes}
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
content = <div className="mx_MyGroups_error">
|
||||
{_t('Error whilst fetching joined groups')}
|
||||
</div>;
|
||||
} else {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return <div className="mx_MyGroups">
|
||||
<SimpleRoomHeader title={ _t("Groups") } />
|
||||
<div className='mx_MyGroups_joinCreateBox'>
|
||||
<div className="mx_MyGroups_createBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Create a new group')}
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_t(
|
||||
'Create a group to represent your community! '+
|
||||
'Define a set of rooms and your own custom homepage '+
|
||||
'to mark out your space in the Matrix universe.',
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Join an existing group')}
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_tJsx(
|
||||
'To join an exisitng group you\'ll have to '+
|
||||
'know its group identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{sub}</i>,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{content}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
}));
|
@ -33,9 +33,6 @@ module.exports = React.createClass({
|
||||
// the room this statusbar is representing.
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// a TabComplete object
|
||||
tabComplete: React.PropTypes.object.isRequired,
|
||||
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
|
||||
@ -143,12 +140,9 @@ module.exports = React.createClass({
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall ||
|
||||
this.props.tabComplete.isTabCompleting()
|
||||
this.props.hasActiveCall
|
||||
) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.props.tabCompleteEntries) {
|
||||
return STATUS_BAR_HIDDEN;
|
||||
} else if (this.props.unsentMessageError) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
@ -237,8 +231,6 @@ module.exports = React.createClass({
|
||||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
@ -259,20 +251,6 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.tabComplete.isTabCompleting()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_tabCompleteBar">
|
||||
<div className="mx_RoomStatusBar_tabCompleteWrapper">
|
||||
<TabCompleteBar tabComplete={this.props.tabComplete} />
|
||||
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
|
||||
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
||||
{_t('Auto-complete')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.unsentMessageError) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
|
@ -22,7 +22,7 @@ limitations under the License.
|
||||
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var classNames = require("classnames");
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
import { _t } from '../../languageHandler';
|
||||
@ -33,7 +33,6 @@ var ContentMessages = require("../../ContentMessages");
|
||||
var Modal = require("../../Modal");
|
||||
var sdk = require('../../index');
|
||||
var CallHandler = require('../../CallHandler');
|
||||
var TabComplete = require("../../TabComplete");
|
||||
var Resend = require("../../Resend");
|
||||
var dis = require("../../dispatcher");
|
||||
var Tinter = require("../../Tinter");
|
||||
@ -47,13 +46,14 @@ import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
|
||||
var DEBUG = false;
|
||||
let DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
||||
const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
|
||||
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
var debuglog = console.log.bind(console);
|
||||
} else {
|
||||
var debuglog = function() {};
|
||||
debuglog = console.log.bind(console);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -93,6 +93,7 @@ module.exports = React.createClass({
|
||||
roomId: null,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
|
||||
// The event to be scrolled to initially
|
||||
initialEventId: null,
|
||||
@ -112,6 +113,7 @@ module.exports = React.createClass({
|
||||
callState: null,
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
showApps: false,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
@ -141,15 +143,6 @@ module.exports = React.createClass({
|
||||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
|
||||
this.tabComplete = new TabComplete({
|
||||
allowLooping: false,
|
||||
autoEnterTabComplete: true,
|
||||
onClickCompletes: true,
|
||||
onStateChange: (isCompleting) => {
|
||||
this.forceUpdate();
|
||||
},
|
||||
});
|
||||
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
@ -168,8 +161,14 @@ module.exports = React.createClass({
|
||||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
};
|
||||
|
||||
// finished joining, start waiting for a room and show a spinner. See onRoom.
|
||||
newState.waitingForRoom = this.state.joining && !newState.joining &&
|
||||
!RoomViewStore.getJoinError();
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
@ -177,12 +176,11 @@ module.exports = React.createClass({
|
||||
newState.roomAlias,
|
||||
'loading?', newState.roomLoading,
|
||||
'joining?', newState.joining,
|
||||
'initial?', initial,
|
||||
'waiting?', newState.waitingForRoom,
|
||||
'shouldPeek?', newState.shouldPeek,
|
||||
);
|
||||
|
||||
// finished joining, start waiting for a room and show a spinner. See onRoom.
|
||||
newState.waitingForRoom = this.state.joining && !newState.joining &&
|
||||
!RoomViewStore.getJoinError();
|
||||
|
||||
// NB: This does assume that the roomID will not change for the lifetime of
|
||||
// the RoomView instance
|
||||
if (initial) {
|
||||
@ -228,13 +226,16 @@ module.exports = React.createClass({
|
||||
// making it impossible to indicate a newly joined room.
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this._updateAutoComplete(room);
|
||||
this.tabComplete.loadEntries(room);
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
showApps: this._shouldShowApps(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
if (!this.state.joining && this.state.roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room) {
|
||||
} else if (!room && this.state.shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
@ -262,13 +263,22 @@ module.exports = React.createClass({
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
},
|
||||
|
||||
_shouldShowApps: function(room) {
|
||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||
|
||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
// any valid widget = show apps
|
||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var call = this._getCallForRoom();
|
||||
var callState = call ? call.call_state : "ended";
|
||||
@ -449,13 +459,13 @@ module.exports = React.createClass({
|
||||
this._updateConfCallNotification();
|
||||
|
||||
this.setState({
|
||||
callState: callState
|
||||
callState: callState,
|
||||
});
|
||||
|
||||
break;
|
||||
case 'forward_event':
|
||||
case 'appsDrawer':
|
||||
this.setState({
|
||||
forwardingEvent: payload.content,
|
||||
showApps: payload.show,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -498,9 +508,7 @@ module.exports = React.createClass({
|
||||
// update the tab complete list as it depends on who most recently spoke,
|
||||
// and that has probably just changed
|
||||
if (ev.sender) {
|
||||
this.tabComplete.onMemberSpoke(ev.sender);
|
||||
// nb. we don't need to update the new autocomplete here since
|
||||
// its results are currently ordered purely by search score.
|
||||
UserProvider.getInstance().onUserSpoke(ev.sender);
|
||||
}
|
||||
},
|
||||
|
||||
@ -523,6 +531,7 @@ module.exports = React.createClass({
|
||||
this._warnAboutEncryption(room);
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
UserProvider.getInstance().setUserListFromRoom(room);
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function(room) {
|
||||
@ -698,8 +707,7 @@ module.exports = React.createClass({
|
||||
this._updateConfCallNotification();
|
||||
|
||||
// refresh the tab complete list
|
||||
this.tabComplete.loadEntries(this.state.room);
|
||||
this._updateAutoComplete(this.state.room);
|
||||
UserProvider.getInstance().setUserListFromRoom(this.state.room);
|
||||
|
||||
// if we are now a member of the room, where we were not before, that
|
||||
// means we have finished joining a room we were previously peeking
|
||||
@ -767,7 +775,7 @@ module.exports = React.createClass({
|
||||
|
||||
onSearchResultsFillRequest: function(backwards) {
|
||||
if (!backwards) {
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (this.state.searchResults.next_batch) {
|
||||
@ -777,7 +785,7 @@ module.exports = React.createClass({
|
||||
return this._handleSearchResult(searchPromise);
|
||||
} else {
|
||||
debuglog("no more search results");
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -838,7 +846,7 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
q().then(() => {
|
||||
Promise.resolve().then(() => {
|
||||
const signUrl = this.props.thirdPartyInvite ?
|
||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||
dis.dispatch({
|
||||
@ -857,7 +865,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
}
|
||||
}
|
||||
return q();
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
|
||||
@ -1164,8 +1172,13 @@ module.exports = React.createClass({
|
||||
this.updateTint();
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
forwardingEvent: null,
|
||||
});
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
@ -1419,14 +1432,6 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_updateAutoComplete: function(room) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const members = room.getJoinedMembers().filter(function(member) {
|
||||
if (member.userId !== myUserId) return true;
|
||||
});
|
||||
UserProvider.getInstance().setUserList(members);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
@ -1463,7 +1468,7 @@ module.exports = React.createClass({
|
||||
|
||||
// We have no room object for this room, only the ID.
|
||||
// We've got to this room by following a link, possibly a third party invite.
|
||||
var room_alias = this.state.room_alias;
|
||||
const roomAlias = this.state.roomAlias;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header"
|
||||
@ -1476,7 +1481,7 @@ module.exports = React.createClass({
|
||||
onForgetClick={ this.onForgetClick }
|
||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={room_alias}
|
||||
roomAlias={roomAlias}
|
||||
spinner={previewBarSpinner}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
@ -1554,7 +1559,6 @@ module.exports = React.createClass({
|
||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
tabComplete={this.tabComplete}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
unsentMessageError={this.state.unsentMessageError}
|
||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||
@ -1576,7 +1580,7 @@ module.exports = React.createClass({
|
||||
} else if (this.state.uploadingRoomSettings) {
|
||||
aux = <Loader/>;
|
||||
} else if (this.state.forwardingEvent !== null) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
|
||||
@ -1607,11 +1611,13 @@ module.exports = React.createClass({
|
||||
|
||||
var auxPanel = (
|
||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
onResize={this.onChildResize} >
|
||||
onResize={this.onChildResize}
|
||||
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
||||
{ aux }
|
||||
</AuxPanel>
|
||||
);
|
||||
@ -1624,8 +1630,13 @@ module.exports = React.createClass({
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
||||
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
|
||||
room={this.state.room}
|
||||
onResize={this.onChildResize}
|
||||
uploadFile={this.uploadFile}
|
||||
callState={this.state.callState}
|
||||
opacity={ this.props.opacity }
|
||||
showApps={ this.state.showApps }
|
||||
/>;
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var KeyCode = require('../../KeyCode');
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
||||
return {
|
||||
stickyBottom: true,
|
||||
startAtBottom: true,
|
||||
onFillRequest: function(backwards) { return q(false); },
|
||||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onScroll: function() {},
|
||||
};
|
||||
@ -386,19 +386,12 @@ module.exports = React.createClass({
|
||||
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||
|
||||
// onFillRequest can end up calling us recursively (via onScroll
|
||||
// events) so make sure we set this before firing off the call. That
|
||||
// does present the risk that we might not ever actually fire off the
|
||||
// fill request, so wrap it in a try/catch.
|
||||
// events) so make sure we set this before firing off the call.
|
||||
this._pendingFillRequests[dir] = true;
|
||||
var fillPromise;
|
||||
try {
|
||||
fillPromise = this.props.onFillRequest(backwards);
|
||||
} catch (e) {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
throw e;
|
||||
}
|
||||
|
||||
q.finally(fillPromise, () => {
|
||||
Promise.try(() => {
|
||||
return this.props.onFillRequest(backwards);
|
||||
}).finally(() => {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
}).then((hasMoreResults) => {
|
||||
if (this.unmounted) {
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var EventTimeline = Matrix.EventTimeline;
|
||||
@ -314,13 +314,13 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
if (!this.state[canPaginateKey]) {
|
||||
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if(!this._timelineWindow.canPaginate(dir)) {
|
||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||
this.setState({[canPaginateKey]: false});
|
||||
return q(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||
@ -353,9 +353,9 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
onMessageListScroll: function() {
|
||||
onMessageListScroll: function(e) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
this.props.onScroll(e);
|
||||
}
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
|
@ -21,7 +21,8 @@ const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const PlatformPeg = require("../../PlatformPeg");
|
||||
const Modal = require('../../Modal');
|
||||
const dis = require("../../dispatcher");
|
||||
const q = require('q');
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import Promise from 'bluebird';
|
||||
const packageJson = require('../../../package.json');
|
||||
const UserSettingsStore = require('../../UserSettingsStore');
|
||||
const CallMediaHandler = require('../../CallMediaHandler');
|
||||
@ -93,8 +94,12 @@ const SETTINGS_LABELS = [
|
||||
label: 'Hide removed messages',
|
||||
},
|
||||
{
|
||||
id: 'disableMarkdown',
|
||||
label: 'Disable markdown formatting',
|
||||
id: 'enableSyntaxHighlightLanguageDetection',
|
||||
label: 'Enable automatic language detection for syntax highlighting',
|
||||
},
|
||||
{
|
||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||
label: 'Automatically replace plain text Emoji',
|
||||
},
|
||||
/*
|
||||
{
|
||||
@ -173,9 +178,6 @@ module.exports = React.createClass({
|
||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||
referralBaseUrl: React.PropTypes.string,
|
||||
|
||||
// true if RightPanel is collapsed
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
|
||||
// Team token for the referral link. If falsy, the referral section will
|
||||
// not appear
|
||||
teamToken: React.PropTypes.string,
|
||||
@ -205,7 +207,7 @@ module.exports = React.createClass({
|
||||
this._addThreepid = null;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
q().then(() => {
|
||||
Promise.resolve().then(() => {
|
||||
return PlatformPeg.get().getAppVersion();
|
||||
}).done((appVersion) => {
|
||||
if (this._unmounted) return;
|
||||
@ -250,6 +252,12 @@ module.exports = React.createClass({
|
||||
this.setState({
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
});
|
||||
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
this._setStateFromSessionStore,
|
||||
);
|
||||
this._setStateFromSessionStore();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@ -276,12 +284,28 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
// `UserSettings` assumes that the client peg will not be null, so give it some
|
||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||
//
|
||||
// This is required because `UserSettings` maintains its own state and if this state
|
||||
// updates (e.g. during _setStateFromSessionStore) after the client peg has been made
|
||||
// null (during logout), then it will attempt to re-render and throw errors.
|
||||
shouldComponentUpdate: function() {
|
||||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
_setStateFromSessionStore: function() {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
},
|
||||
|
||||
_electronSettings: function(ev, settings) {
|
||||
this.setState({ electron_settings: settings });
|
||||
},
|
||||
|
||||
_refreshMediaDevices: function() {
|
||||
q().then(() => {
|
||||
Promise.resolve().then(() => {
|
||||
return CallMediaHandler.getDevices();
|
||||
}).then((mediaDevices) => {
|
||||
// console.log("got mediaDevices", mediaDevices, this._unmounted);
|
||||
@ -296,7 +320,7 @@ module.exports = React.createClass({
|
||||
|
||||
_refreshFromServer: function() {
|
||||
const self = this;
|
||||
q.all([
|
||||
Promise.all([
|
||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
|
||||
]).done(function(resps) {
|
||||
self.setState({
|
||||
@ -548,15 +572,16 @@ module.exports = React.createClass({
|
||||
});
|
||||
// reject the invites
|
||||
const promises = rooms.map((room) => {
|
||||
return MatrixClientPeg.get().leave(room.roomId);
|
||||
return MatrixClientPeg.get().leave(room.roomId).catch((e) => {
|
||||
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||
// after trying to reject all the invites.
|
||||
});
|
||||
});
|
||||
// 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(() => {
|
||||
Promise.all(promises).then(() => {
|
||||
this.setState({
|
||||
rejectingInvites: false,
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
@ -626,6 +651,10 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_renderUserInterfaceSettings: function() {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) =>
|
||||
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Interface") }</h3>
|
||||
@ -633,8 +662,21 @@ module.exports = React.createClass({
|
||||
{ this._renderUrlPreviewSelector() }
|
||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||
{ THEMES.map( this._renderThemeSelector ) }
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{ this._renderLanguageSetting() }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -868,6 +910,21 @@ module.exports = React.createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderCheckUpdate: function() {
|
||||
const platform = PlatformPeg.get();
|
||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||
return <div>
|
||||
<h3>{_t('Updates')}</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||
{_t('Check for update')}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <div />;
|
||||
},
|
||||
|
||||
_renderBulkOptions: function() {
|
||||
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||
return r.hasMembershipState(this._me, "invite");
|
||||
@ -1168,7 +1225,6 @@ module.exports = React.createClass({
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader
|
||||
title={ _t("Settings") }
|
||||
collapsedRhs={ this.props.collapsedRhs }
|
||||
onCancelClick={ this.props.onClose }
|
||||
/>
|
||||
|
||||
@ -1209,10 +1265,14 @@ module.exports = React.createClass({
|
||||
<h3>{ _t("Account") }</h3>
|
||||
|
||||
<div className="mx_UserSettings_section cadcampoHide">
|
||||
|
||||
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
{ _t("Sign out") }
|
||||
</AccessibleButton>
|
||||
{ this.state.userHasGeneratedPassword ?
|
||||
<div className="mx_UserSettings_passwordWarning">
|
||||
{ _t("To return to your account in future you need to set a password") }
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{accountJsx}
|
||||
</div>
|
||||
@ -1266,6 +1326,8 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this._renderCheckUpdate()}
|
||||
|
||||
{this._renderClearCache()}
|
||||
|
||||
{this._renderDeactivateAccount()}
|
||||
|
@ -72,9 +72,14 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
@ -87,6 +92,9 @@ module.exports = React.createClass({
|
||||
).then((data) => {
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
if(this._unmounted) {
|
||||
return;
|
||||
}
|
||||
let errorText;
|
||||
|
||||
// Some error strings only apply for logging in
|
||||
@ -109,8 +117,11 @@ module.exports = React.createClass({
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if(this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
@ -180,7 +180,7 @@ module.exports = React.createClass({
|
||||
// will just nop. The point of this being we might not have the email address
|
||||
// that the user registered with at this stage (depending on whether this
|
||||
// is the client they initiated registration).
|
||||
let trackPromise = q(null);
|
||||
let trackPromise = Promise.resolve(null);
|
||||
if (this._rtsClient && extra.emailSid) {
|
||||
// Track referral if this.props.referrer set, get team_token in order to
|
||||
// retrieve team config and see welcome page etc.
|
||||
@ -218,29 +218,29 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
trackPromise.then((teamToken) => {
|
||||
this.props.onLoggedIn({
|
||||
return this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
}, teamToken);
|
||||
}).then(() => {
|
||||
return this._setupPushers();
|
||||
}).then((cli) => {
|
||||
return this._setupPushers(cli);
|
||||
});
|
||||
},
|
||||
|
||||
_setupPushers: function() {
|
||||
_setupPushers: function(matrixClient) {
|
||||
if (!this.props.brand) {
|
||||
return q();
|
||||
return Promise.resolve();
|
||||
}
|
||||
return MatrixClientPeg.get().getPushers().then((resp)=>{
|
||||
return matrixClient.getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
|
||||
matrixClient.setPusher(emailPusher).done(() => {
|
||||
console.log("Set email branding to " + this.props.brand);
|
||||
}, (error) => {
|
||||
console.error("Couldn't set email branding: " + error);
|
||||
|
66
src/components/views/avatars/GroupAvatar.js
Normal file
66
src/components/views/avatars/GroupAvatar.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
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 sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupAvatar',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getGroupAvatarUrl: function() {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||
this.props.groupAvatarUrl,
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar
|
||||
name={this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
@ -72,7 +72,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getRoomAvatarUrl: function(props) {
|
||||
if (!this.props.room) return null;
|
||||
if (!props.room) return null;
|
||||
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
@ -84,7 +84,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!this.props.room) return null;
|
||||
if (!props.room) return null;
|
||||
|
||||
var mlist = props.room.currentState.members;
|
||||
var userIds = [];
|
||||
@ -126,9 +126,16 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getFallbackAvatar: function(props) {
|
||||
if (!this.props.room) return null;
|
||||
let roomId = null;
|
||||
if (props.oobData && props.oobData.roomId) {
|
||||
roomId = this.props.oobData.roomId;
|
||||
} else if (props.room) {
|
||||
roomId = props.room.roomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Avatar.defaultAvatarUrlForString(props.room.roomId);
|
||||
return Avatar.defaultAvatarUrlForString(roomId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -23,7 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
@ -178,7 +178,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onQueryChanged: function(ev) {
|
||||
const query = ev.target.value.toLowerCase();
|
||||
const query = ev.target.value;
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
@ -271,10 +271,11 @@ module.exports = React.createClass({
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
const queryLowercase = query.toLowerCase();
|
||||
const results = [];
|
||||
MatrixClientPeg.get().getUsers().forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(query) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(query) === -1
|
||||
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -497,7 +498,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
return q.delay(500).then(() => {
|
||||
return Promise.delay(500).then(() => {
|
||||
if (cancelled) return null;
|
||||
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||
}).then((res) => {
|
||||
|
199
src/components/views/dialogs/CreateGroupDialog.js
Normal file
199
src/components/views/dialogs/CreateGroupDialog.js
Normal file
@ -0,0 +1,199 @@
|
||||
/*
|
||||
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 sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// We match fairly liberally and leave it up to the server to reject if
|
||||
// there are invalid characters etc.
|
||||
const GROUP_REGEX = /^\+(.*?):(.*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
groupName: '',
|
||||
groupId: '',
|
||||
groupError: null,
|
||||
creating: false,
|
||||
createError: null,
|
||||
};
|
||||
},
|
||||
|
||||
_onGroupNameChange: function(e) {
|
||||
this.setState({
|
||||
groupName: e.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
_onGroupIdChange: function(e) {
|
||||
this.setState({
|
||||
groupId: e.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
_onGroupIdBlur: function(e) {
|
||||
this._checkGroupId();
|
||||
},
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
let error = null;
|
||||
if (parsedGroupId === null) {
|
||||
error = _t(
|
||||
"Group IDs must be of the form +localpart:%(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
} else {
|
||||
const domain = parsedGroupId[1];
|
||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||
error = _t(
|
||||
"It is currently only possible to create groups on your own home server: "+
|
||||
"use a group ID ending with %(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
});
|
||||
return error;
|
||||
},
|
||||
|
||||
_onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this._checkGroupId()) return;
|
||||
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
const profile = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({creating: true});
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: parsedGroupId[0],
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
this.setState({createError: e});
|
||||
}).finally(() => {
|
||||
this.setState({creating: false});
|
||||
}).done();
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a string that may be a group ID
|
||||
* If the string is a valid group ID, return a list of [localpart, domain],
|
||||
* otherwise return null.
|
||||
*
|
||||
* @param {string} groupId The ID of the group
|
||||
* @return {string[]} array of localpart, domain
|
||||
*/
|
||||
_parseGroupId: function(groupId) {
|
||||
const matches = GROUP_REGEX.exec(this.state.groupId);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
if (this.state.creating) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let createErrorNode;
|
||||
if (this.state.createError) {
|
||||
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
||||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{_t('Room creation failed')}</div>
|
||||
<div>{this.state.createError.message}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Group')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
autoFocus={true} size="64"
|
||||
placeholder={_t('Example')}
|
||||
onChange={this._onGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
size="64"
|
||||
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{this.state.groupIdError}
|
||||
</div>
|
||||
{createErrorNode}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
172
src/components/views/dialogs/KeyShareDialog.js
Normal file
172
src/components/views/dialogs/KeyShareDialog.js
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
* an unverified device.
|
||||
*
|
||||
* onFinished is called with `true` if the key should be shared, `false` if it
|
||||
* should not, and `undefined` if the dialog is cancelled. (In other words:
|
||||
* truthy: do the key share. falsy: don't share the keys).
|
||||
*/
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
deviceId: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
deviceInfo: null,
|
||||
wasNewDevice: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
const userId = this.props.userId;
|
||||
const deviceId = this.props.deviceId;
|
||||
|
||||
// give the client a chance to refresh the device list
|
||||
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
|
||||
if (this._unmounted) { return; }
|
||||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if(!deviceInfo) {
|
||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasNewDevice = !deviceInfo.isKnown();
|
||||
|
||||
this.setState({
|
||||
deviceInfo: deviceInfo,
|
||||
wasNewDevice: wasNewDevice,
|
||||
});
|
||||
|
||||
// if the device was new before, it's not any more.
|
||||
if (wasNewDevice) {
|
||||
this.props.matrixClient.setDeviceKnown(
|
||||
userId,
|
||||
deviceId,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
|
||||
_onVerifyClicked: function() {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
|
||||
console.log("KeyShareDialog: Starting verify dialog");
|
||||
Modal.createDialog(DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.deviceInfo,
|
||||
onFinished: (verified) => {
|
||||
if (verified) {
|
||||
// can automatically share the keys now.
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onShareClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'share'");
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onIgnoreClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'ignore'");
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_renderContent: function() {
|
||||
const displayName = this.state.deviceInfo.getDisplayName() ||
|
||||
this.state.deviceInfo.deviceId;
|
||||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = "You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.";
|
||||
} else {
|
||||
text = "Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.";
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{text}</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
{_t('Start verification')}
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{_t('Share without verifying')}
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{_t('Ignore request')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let content;
|
||||
|
||||
if (this.state.deviceInfo) {
|
||||
content = this._renderContent();
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t('Loading device info...')}</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
@ -154,7 +154,7 @@ export default React.createClass({
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value={_t("Cancel")}
|
||||
value={_t("Skip")}
|
||||
onClick={this.onCancelled}
|
||||
/>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
191
src/components/views/elements/AppTile.js
Normal file
191
src/components/views/elements/AppTile.js
Normal file
@ -0,0 +1,191 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import url from 'url';
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
|
||||
propTypes: {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
url: React.PropTypes.string.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
room: React.PropTypes.object.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url: "",
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
loading: false,
|
||||
widgetUrl: this.props.url,
|
||||
error: null,
|
||||
deleting: false,
|
||||
};
|
||||
},
|
||||
|
||||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||
isScalarUrl: function() {
|
||||
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (!this.isScalarUrl()) {
|
||||
return;
|
||||
}
|
||||
// Fetch the token before loading the iframe as we need to mangle the URL
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
this._scalarClient = new ScalarAuthClient();
|
||||
this._scalarClient.getScalarToken().done((token) => {
|
||||
// Append scalar_token as a query param
|
||||
const u = url.parse(this.props.url);
|
||||
if (!u.search) {
|
||||
u.search = "?scalar_token=" + encodeURIComponent(token);
|
||||
} else {
|
||||
u.search += "&scalar_token=" + encodeURIComponent(token);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: u.format(),
|
||||
loading: false,
|
||||
});
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
error: err.message,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onEditClick: function(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
|
||||
_onDeleteClick: function() {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
|
||||
formatAppTileName: function() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let appTileBody;
|
||||
|
||||
// Don't render widget if it is in the process of being deleted
|
||||
if (this.state.deleting) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
appTileBody = (
|
||||
<div> Loading... </div>
|
||||
);
|
||||
} else {
|
||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
|
||||
// this would only be for content hosted on the same origin as the riot client: anything
|
||||
// hosted on the same origin as the client will get the same access as if you clicked
|
||||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts";
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<iframe ref="appFrame" src={safeWidgetUrl} allowFullScreen="true"
|
||||
sandbox={sandboxFlags}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient);
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className="mx_AppTileMenuBar">
|
||||
{this.formatAppTileName()}
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>β</span>
|
||||
{/* Edit widget */}
|
||||
{showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8" alt="Edit"
|
||||
onClick={this._onEditClick}
|
||||
/>}
|
||||
|
||||
{/* Delete widget */}
|
||||
<img src="img/cancel.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
||||
width="8" height="8" alt={_t("Cancel")}
|
||||
onClick={this._onDeleteClick}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{appTileBody}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
* A component which wraps an EditableText, with a spinner while updates take
|
||||
@ -148,5 +148,5 @@ EditableTextContainer.defaultProps = {
|
||||
initialValue: "",
|
||||
placeholder: "",
|
||||
blurToSubmit: false,
|
||||
onSubmit: function(v) {return q(); },
|
||||
onSubmit: function(v) {return Promise.resolve(); },
|
||||
};
|
||||
|
117
src/components/views/elements/Pill.js
Normal file
117
src/components/views/elements/Pill.js
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
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';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+]).*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
statics: {
|
||||
isPillUrl: (url) => {
|
||||
return !!REGEX_MATRIXTO.exec(url);
|
||||
},
|
||||
isMessagePillUrl: (url) => {
|
||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
inMessage: PropTypes.bool,
|
||||
// The room in which this pill is being rendered
|
||||
room: PropTypes.instanceOf(Room),
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
let regex = REGEX_MATRIXTO;
|
||||
if (this.props.inMessage) {
|
||||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
}
|
||||
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = regex.exec(this.props.url) || [];
|
||||
|
||||
const resource = matrixToMatch[1]; // The room/user ID
|
||||
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||
|
||||
// Default to the room/user ID
|
||||
let linkText = resource;
|
||||
|
||||
const isUserPill = prefix === '@';
|
||||
const isRoomPill = prefix === '#' || prefix === '!';
|
||||
|
||||
let avatar = null;
|
||||
let userId;
|
||||
if (isUserPill) {
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
// TODO: This could be improved by doing an async profile lookup
|
||||
const member = this.props.room.getMember(resource) ||
|
||||
new RoomMember(null, resource);
|
||||
if (member) {
|
||||
userId = member.userId;
|
||||
linkText = member.name;
|
||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||
}
|
||||
} else if (isRoomPill) {
|
||||
const room = prefix === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
return r.getAliases().includes(resource);
|
||||
}) : MatrixClientPeg.get().getRoom(resource);
|
||||
|
||||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||
}
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_UserPill": isUserPill,
|
||||
"mx_RoomPill": isRoomPill,
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||
});
|
||||
|
||||
if ((isUserPill || isRoomPill) && avatar) {
|
||||
return this.props.inMessage ?
|
||||
<a className={classes} href={this.props.url}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
</a> :
|
||||
<span className={classes}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
</span>;
|
||||
} else {
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
@ -46,6 +46,10 @@ module.exports = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._captchaWidgetId = null;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
||||
// so we do this instead.
|
||||
@ -75,6 +79,10 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._resetRecaptcha();
|
||||
},
|
||||
|
||||
_renderRecaptcha: function(divId) {
|
||||
if (!global.grecaptcha) {
|
||||
console.error("grecaptcha not loaded!");
|
||||
@ -90,12 +98,18 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
console.log("Rendering to %s", divId);
|
||||
global.grecaptcha.render(divId, {
|
||||
this._captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
sitekey: publicKey,
|
||||
callback: this.props.onCaptchaResponse,
|
||||
});
|
||||
},
|
||||
|
||||
_resetRecaptcha: function() {
|
||||
if (this._captchaWidgetId !== null) {
|
||||
global.grecaptcha.reset(this._captchaWidgetId);
|
||||
}
|
||||
},
|
||||
|
||||
_onCaptchaLoaded: function() {
|
||||
console.log("Loaded recaptcha script.");
|
||||
try {
|
||||
|
@ -69,10 +69,19 @@ class PasswordLogin extends React.Component {
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||
this.props.onSubmit(
|
||||
'', // XXX: Synapse breaks if you send null here:
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
null,
|
||||
null,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
||||
this.fixupHeight();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = q(null);
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.thumbnail_url) {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||
} else {
|
||||
return null;
|
||||
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
var thumbnailPromise = q(null);
|
||||
var thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file
|
||||
|
@ -29,6 +29,10 @@ import Modal from '../../../Modal';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
@ -79,6 +83,10 @@ module.exports = React.createClass({
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
this.pillifyLinks(this.refs.content.children);
|
||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
@ -90,7 +98,18 @@ module.exports = React.createClass({
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||
highlight.highlightBlock(blocks[i])
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
|
||||
return cl.startsWith('language-');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
@ -131,9 +150,15 @@ module.exports = React.createClass({
|
||||
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||
var links = this.findLinks(this.refs.content.children);
|
||||
if (links.length) {
|
||||
this.setState({ links: links.map((link)=>{
|
||||
return link.getAttribute("href");
|
||||
})});
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
links = links.filter((link) => {
|
||||
if (seen.has(link)) return false;
|
||||
seen.add(link);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.setState({ links: links });
|
||||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
@ -144,14 +169,38 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
if (Pill.isMessagePillUrl(href)) {
|
||||
const pillContainer = document.createElement('span');
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill url={href} inMessage={true} room={room}/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
node.parentNode.replaceChild(pillContainer, node);
|
||||
}
|
||||
} else if (node.children && node.children.length) {
|
||||
this.pillifyLinks(node.children);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findLinks: function(nodes) {
|
||||
var links = [];
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (node.tagName === "A" && node.getAttribute("href"))
|
||||
{
|
||||
if (this.isLinkPreviewable(node)) {
|
||||
links.push(node);
|
||||
links.push(node.getAttribute("href"));
|
||||
}
|
||||
}
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
@ -213,10 +262,9 @@ module.exports = React.createClass({
|
||||
|
||||
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)', ''),
|
||||
action: 'insert_mention',
|
||||
user_id: mxEvent.getSender(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
var ObjectUtils = require("../../../ObjectUtils");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
promises = [q.all(promises).then(
|
||||
promises = [Promise.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: this.state.canonicalAlias
|
||||
|
@ -13,7 +13,7 @@ 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 q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
@ -72,7 +72,7 @@ module.exports = React.createClass({
|
||||
|
||||
saveSettings: function() { // : Promise
|
||||
if (!this.state.hasChanged) {
|
||||
return q(); // They didn't explicitly give a color to save.
|
||||
return Promise.resolve(); // They didn't explicitly give a color to save.
|
||||
}
|
||||
var originalState = this.getInitialState();
|
||||
if (originalState.primary_color !== this.state.primary_color ||
|
||||
@ -92,7 +92,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
});
|
||||
}
|
||||
return q(); // no color diff
|
||||
return Promise.resolve(); // no color diff
|
||||
},
|
||||
|
||||
_getColorIndex: function(scheme) {
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require("../../../index");
|
||||
|
197
src/components/views/rooms/AppsDrawer.js
Normal file
197
src/components/views/rooms/AppsDrawer.js
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AppsDrawer',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
apps: this._getApps(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
if (this.state.apps && this.state.apps.length < 1) {
|
||||
this.onClickAddWidget();
|
||||
}
|
||||
// TODO -- Handle Scalar errors
|
||||
// },
|
||||
// (err) => {
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
// });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
ScalarMessaging.stopListening();
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
};
|
||||
|
||||
if(app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
}
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
}
|
||||
this._updateApps();
|
||||
},
|
||||
|
||||
_getApps: function() {
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return appsStateEvents.filter((ev) => {
|
||||
return ev.getContent().type && ev.getContent().url;
|
||||
}).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent());
|
||||
});
|
||||
},
|
||||
|
||||
_updateApps: function() {
|
||||
const apps = this._getApps();
|
||||
if (apps.length < 1) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
apps: apps,
|
||||
});
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||
null;
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const apps = this.state.apps.map(
|
||||
(app, index, arr) => {
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
/>;
|
||||
});
|
||||
|
||||
const addWidget = this.state.apps && this.state.apps.length < 2 &&
|
||||
(<div onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="mx_AddWidget_button"
|
||||
title={_t('Add a widget')}>
|
||||
[+] {_t('Add a widget')}
|
||||
</div>);
|
||||
|
||||
return (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
{apps}
|
||||
</div>
|
||||
{addWidget}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
@ -5,7 +5,8 @@ import flatMap from 'lodash/flatMap';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sdk from '../../../index';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
@ -39,26 +40,62 @@ export default class Autocomplete extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(props, state) {
|
||||
if (props.query === this.props.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.complete(props.query, props.selection);
|
||||
}
|
||||
|
||||
async complete(query, selection) {
|
||||
let forceComplete = this.state.forceComplete;
|
||||
const completionPromise = getCompletions(query, selection, forceComplete);
|
||||
this.completionPromise = completionPromise;
|
||||
const completions = await this.completionPromise;
|
||||
|
||||
// There's a newer completion request, so ignore results.
|
||||
if (completionPromise !== this.completionPromise) {
|
||||
componentWillReceiveProps(newProps, state) {
|
||||
// Query hasn't changed so don't try to complete it
|
||||
if (newProps.query === this.props.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const completionList = flatMap(completions, provider => provider.completions);
|
||||
this.complete(newProps.query, newProps.selection);
|
||||
}
|
||||
|
||||
complete(query, selection) {
|
||||
this.queryRequested = query;
|
||||
if (this.debounceCompletionsRequest) {
|
||||
clearTimeout(this.debounceCompletionsRequest);
|
||||
}
|
||||
if (query === "") {
|
||||
this.setState({
|
||||
// Clear displayed completions
|
||||
completions: [],
|
||||
completionList: [],
|
||||
// Reset selected completion
|
||||
selectionOffset: COMPOSER_SELECTED,
|
||||
// Hide the autocomplete box
|
||||
hide: true,
|
||||
});
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
||||
|
||||
// Don't debounce if we are already showing completions
|
||||
if (this.state.completions.length > 0 || this.state.forceComplete) {
|
||||
autocompleteDelay = 0;
|
||||
}
|
||||
|
||||
const deferred = Promise.defer();
|
||||
this.debounceCompletionsRequest = setTimeout(() => {
|
||||
this.processQuery(query, selection).then(() => {
|
||||
deferred.resolve();
|
||||
});
|
||||
}, autocompleteDelay);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
processQuery(query, selection) {
|
||||
return getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
return;
|
||||
}
|
||||
this.processCompletions(completions);
|
||||
});
|
||||
}
|
||||
|
||||
processCompletions(completions) {
|
||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||
|
||||
// Reset selection when completion list becomes empty.
|
||||
let selectionOffset = COMPOSER_SELECTED;
|
||||
@ -69,33 +106,26 @@ export default class Autocomplete extends React.Component {
|
||||
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||
selectionOffset = completionList.findIndex(
|
||||
completion => completion.completion === currentSelection);
|
||||
(completion) => completion.completion === currentSelection);
|
||||
if (selectionOffset === -1) {
|
||||
selectionOffset = COMPOSER_SELECTED;
|
||||
} else {
|
||||
selectionOffset++; // selectionOffset is 1-indexed!
|
||||
}
|
||||
} else {
|
||||
// If no completions were returned, we should turn off force completion.
|
||||
forceComplete = false;
|
||||
}
|
||||
|
||||
let hide = this.state.hide;
|
||||
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
||||
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
|
||||
newMatches = completions.map(completion => !!completion.command.command);
|
||||
|
||||
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
|
||||
if (!isEqual(oldMatches, newMatches)) {
|
||||
hide = false;
|
||||
}
|
||||
// If `completion.command.command` is truthy, then a provider has matched with the query
|
||||
const anyMatches = completions.some((completion) => !!completion.command.command);
|
||||
hide = !anyMatches;
|
||||
|
||||
this.setState({
|
||||
completions,
|
||||
completionList,
|
||||
selectionOffset,
|
||||
hide,
|
||||
forceComplete,
|
||||
// Force complete is turned off each time since we can't edit the query in that case
|
||||
forceComplete: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -146,12 +176,13 @@ export default class Autocomplete extends React.Component {
|
||||
}
|
||||
|
||||
forceComplete() {
|
||||
const done = Q.defer();
|
||||
const done = Promise.defer();
|
||||
this.setState({
|
||||
forceComplete: true,
|
||||
hide: false,
|
||||
}, () => {
|
||||
this.complete(this.props.query, this.props.selection).then(() => {
|
||||
done.resolve();
|
||||
done.resolve(this.countCompletions());
|
||||
});
|
||||
});
|
||||
return done.promise;
|
||||
@ -169,7 +200,7 @@ export default class Autocomplete extends React.Component {
|
||||
}
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset});
|
||||
this.setState({selectionOffset, hide: false});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -185,21 +216,24 @@ export default class Autocomplete extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
setState(state, func) {
|
||||
super.setState(state, func);
|
||||
}
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 1;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
const className = classNames('mx_Autocomplete_Completion', {
|
||||
'selected': position === this.state.selectionOffset,
|
||||
});
|
||||
let componentPosition = position;
|
||||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
let onMouseOver = () => this.setSelection(componentPosition);
|
||||
let onClick = () => {
|
||||
const onMouseOver = () => this.setSelection(componentPosition);
|
||||
const onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onCompletionClicked();
|
||||
};
|
||||
@ -220,7 +254,7 @@ export default class Autocomplete extends React.Component {
|
||||
{completionResult.provider.renderCompletions(completions)}
|
||||
</div>
|
||||
) : null;
|
||||
}).filter(completion => !!completion);
|
||||
}).filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
|
@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -28,6 +30,8 @@ module.exports = React.createClass({
|
||||
propTypes: {
|
||||
// js-sdk room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
showApps: React.PropTypes.bool,
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: React.PropTypes.object,
|
||||
@ -70,10 +74,10 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var CallView = sdk.getComponent("voip.CallView");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const CallView = sdk.getComponent("voip.CallView");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileDropTarget = null;
|
||||
let fileDropTarget = null;
|
||||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
@ -87,14 +91,13 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
let conferenceCallNotification = null;
|
||||
if (this.props.displayConfCallNotification) {
|
||||
let supportedText = '';
|
||||
let joinNode;
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
supportedText = _t(" (unsupported)");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{_tJsx(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
@ -105,7 +108,6 @@ module.exports = React.createClass({
|
||||
]
|
||||
)}
|
||||
</span>);
|
||||
|
||||
}
|
||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var callView = (
|
||||
const callView = (
|
||||
<CallView ref="callView" room={this.props.room}
|
||||
ConferenceHandler={this.props.conferenceHandler}
|
||||
onResize={this.props.onResize}
|
||||
@ -126,8 +128,17 @@ module.exports = React.createClass({
|
||||
/>
|
||||
);
|
||||
|
||||
let appsDrawer = null;
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
|
@ -24,7 +24,7 @@ var Modal = require('../../../Modal');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var TextForEvent = require('../../../TextForEvent');
|
||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
@ -59,7 +59,7 @@ var MAX_READ_AVATARS = 5;
|
||||
// | '--------------------------------------' |
|
||||
// '----------------------------------------------------------'
|
||||
|
||||
module.exports = WithMatrixClient(React.createClass({
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
displayName: 'EventTile',
|
||||
|
||||
propTypes: {
|
||||
@ -193,13 +193,12 @@ module.exports = WithMatrixClient(React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_verifyEvent: function(mxEvent) {
|
||||
var verified = null;
|
||||
|
||||
if (mxEvent.isEncrypted()) {
|
||||
verified = this.props.matrixClient.isEventSenderVerified(mxEvent);
|
||||
_verifyEvent: async function(mxEvent) {
|
||||
if (!mxEvent.isEncrypted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
|
||||
this.setState({
|
||||
verified: verified
|
||||
});
|
||||
@ -336,6 +335,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||
suppressAnimation={this._suppressReadReceiptAnimation}
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
timestamp={receipt.ts}
|
||||
showTwelveHour={this.props.isTwelveHour}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -357,10 +357,10 @@ module.exports = WithMatrixClient(React.createClass({
|
||||
},
|
||||
|
||||
onSenderProfileClick: function(event) {
|
||||
var mxEvent = this.props.mxEvent;
|
||||
const mxEvent = this.props.mxEvent;
|
||||
dis.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
||||
action: 'insert_mention',
|
||||
user_id: mxEvent.getSender(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
|
||||
@ -26,11 +25,6 @@ module.exports = React.createClass({
|
||||
displayName: 'ForwardMessage',
|
||||
|
||||
propTypes: {
|
||||
currentRoomId: React.PropTypes.string.isRequired,
|
||||
|
||||
/* the MatrixEvent to be forwarded */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
onCancelClick: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
@ -44,7 +38,6 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
@ -54,30 +47,9 @@ module.exports = React.createClass({
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
});
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action === 'view_room') {
|
||||
const event = this.props.mxEvent;
|
||||
const Client = MatrixClientPeg.get();
|
||||
Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => {
|
||||
dis.dispatch({action: 'message_sent'});
|
||||
}, (err) => {
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: Client.getRoom(payload.room_id),
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'message_send_failed'});
|
||||
});
|
||||
if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick();
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
|
@ -36,12 +36,12 @@ import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Unread from '../../../Unread';
|
||||
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
|
||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
|
||||
module.exports = WithMatrixClient(React.createClass({
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
||||
propTypes: {
|
||||
@ -136,8 +136,12 @@ module.exports = WithMatrixClient(React.createClass({
|
||||
if (userId == this.props.member.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
var devices = this.props.matrixClient.getStoredDevicesForUser(userId);
|
||||
this.setState({devices: devices});
|
||||
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
this.setState({devices: devices});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -204,14 +208,15 @@ module.exports = WithMatrixClient(React.createClass({
|
||||
|
||||
var client = this.props.matrixClient;
|
||||
var self = this;
|
||||
client.downloadKeys([member.userId], true).finally(function() {
|
||||
client.downloadKeys([member.userId], true).then(() => {
|
||||
return client.getStoredDevicesForUser(member.userId);
|
||||
}).finally(function() {
|
||||
self._cancelDeviceList = null;
|
||||
}).done(function() {
|
||||
}).done(function(devices) {
|
||||
if (cancelled) {
|
||||
// we got cancelled - presumably a different user now
|
||||
return;
|
||||
}
|
||||
var devices = client.getStoredDevicesForUser(member.userId);
|
||||
self._disambiguateDevices(devices);
|
||||
self.setState({devicesLoading: false, devices: devices});
|
||||
}, function(err) {
|
||||
|
@ -17,7 +17,7 @@ var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
var classNames = require('classnames');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var q = require('q');
|
||||
import Promise from 'bluebird';
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var Entities = require("../../../Entities");
|
||||
|
@ -13,16 +13,14 @@ 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');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
@ -32,19 +30,17 @@ export default class MessageComposer extends React.Component {
|
||||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this._tryComplete = this._tryComplete.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||
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: '',
|
||||
@ -57,7 +53,6 @@ export default class MessageComposer extends React.Component {
|
||||
},
|
||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -66,21 +61,12 @@ 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) {
|
||||
@ -127,7 +113,7 @@ export default class MessageComposer extends React.Component {
|
||||
if(shouldUpload) {
|
||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||
if (files) {
|
||||
for(var i=0; i<files.length; i++) {
|
||||
for(let i=0; i<files.length; i++) {
|
||||
this.props.uploadFile(files[i]);
|
||||
}
|
||||
}
|
||||
@ -139,7 +125,7 @@ export default class MessageComposer extends React.Component {
|
||||
}
|
||||
|
||||
onHangupClick() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
return;
|
||||
@ -152,20 +138,68 @@ export default class MessageComposer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onShowAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
|
||||
onHideAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
@ -179,21 +213,6 @@ export default class MessageComposer extends React.Component {
|
||||
this.setState({inputState});
|
||||
}
|
||||
|
||||
onUpArrow() {
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
}
|
||||
|
||||
onDownArrow() {
|
||||
return this.refs.autocomplete.onDownArrow();
|
||||
}
|
||||
|
||||
_tryComplete(): boolean {
|
||||
if (this.refs.autocomplete) {
|
||||
return this.refs.autocomplete.onCompletionClicked();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_onAutocompleteConfirm(range, completion) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.setDisplayedCompletion(range, completion);
|
||||
@ -216,19 +235,18 @@ export default class MessageComposer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
|
||||
var controls = [];
|
||||
const controls = [];
|
||||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
@ -247,16 +265,15 @@ export default class MessageComposer extends React.Component {
|
||||
controls.push(
|
||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||
alt={e2eTitle} title={e2eTitle}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
var callButton, videoCallButton, hangupButton;
|
||||
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||
@ -267,14 +284,29 @@ export default class MessageComposer extends React.Component {
|
||||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
// Apps
|
||||
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||
if (this.props.showApps) {
|
||||
hideAppsButton =
|
||||
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
} else {
|
||||
showAppsButton =
|
||||
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
if (canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
var uploadButton = (
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
@ -290,8 +322,7 @@ export default class MessageComposer extends React.Component {
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src="img/button-text-formatting.svg"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
style={{visibility: this.state.showFormatting ||
|
||||
!UserSettingsStore.isFeatureEnabled('rich_text_editor') ? 'hidden' : 'visible'}}
|
||||
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
|
||||
key="controls_formatting" />
|
||||
);
|
||||
|
||||
@ -300,58 +331,40 @@ export default class MessageComposer extends React.Component {
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
tryComplete={this._tryComplete}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
formattingButton,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
videoCallButton
|
||||
videoCallButton,
|
||||
showAppsButton,
|
||||
hideAppsButton,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
let autoComplete;
|
||||
if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) {
|
||||
autoComplete = <div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
onConfirm={this._onAutocompleteConfirm}
|
||||
query={this.state.autocompleteQuery}
|
||||
selection={this.state.selection} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
const {style, blockType} = this.state.inputState;
|
||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||
name => {
|
||||
(name) => {
|
||||
const active = style.includes(name) || blockType === name;
|
||||
const suffix = active ? '-o-n' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
||||
const className = classNames("mx_MessageComposer_format_button", {
|
||||
mx_MessageComposer_format_button_disabled: disabled,
|
||||
mx_filterFlipColor: true,
|
||||
});
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return <img className={className}
|
||||
title={ _t(name) }
|
||||
onMouseDown={disabled ? null : onFormatButtonClicked}
|
||||
onMouseDown={onFormatButtonClicked}
|
||||
key={name}
|
||||
src={`img/button-text-${name}${suffix}.svg`}
|
||||
height="17" />;
|
||||
@ -365,30 +378,26 @@ export default class MessageComposer extends React.Component {
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||
{formatButtons}
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||
<img title={ _t("Hide Text Formatting Toolbar") }
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||
{formatButtons}
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||
<img title={ _t("Hide Text Formatting Toolbar") }
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageComposer.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,
|
||||
@ -403,5 +412,8 @@ MessageComposer.propTypes = {
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: React.PropTypes.bool,
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,470 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require("react");
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
var Markdown = require("../../../Markdown");
|
||||
|
||||
var TYPING_USER_TIMEOUT = 10000;
|
||||
var TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
export function onSendMessageFailed(err, room) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: room,
|
||||
});
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'MessageComposerInput',
|
||||
|
||||
statics: {
|
||||
// the height we limit the composer to
|
||||
MAX_HEIGHT: 100,
|
||||
},
|
||||
|
||||
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,
|
||||
|
||||
// The text to use a placeholder in the input box
|
||||
placeholder: React.PropTypes.string.isRequired,
|
||||
|
||||
// callback to handle files pasted into the composer
|
||||
onFilesPasted: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.oldScrollHeight = 0;
|
||||
this.markdownEnabled = !UserSettingsStore.getSyncedSetting('disableMarkdown', false);
|
||||
|
||||
var self = this;
|
||||
this.sentHistory = {
|
||||
// The list of typed messages. Index 0 is more recent
|
||||
data: [],
|
||||
// The position in data currently displayed
|
||||
position: -1,
|
||||
// The room the history is for.
|
||||
roomId: null,
|
||||
// The original text before they hit UP
|
||||
originalText: null,
|
||||
// The textarea element to set text to.
|
||||
element: null,
|
||||
|
||||
init: function(element, roomId) {
|
||||
this.roomId = roomId;
|
||||
this.element = element;
|
||||
this.position = -1;
|
||||
var storedData = window.sessionStorage.getItem(
|
||||
"history_" + roomId
|
||||
);
|
||||
if (storedData) {
|
||||
this.data = JSON.parse(storedData);
|
||||
}
|
||||
if (this.roomId) {
|
||||
this.setLastTextEntry();
|
||||
}
|
||||
},
|
||||
|
||||
push: function(text) {
|
||||
// store a message in the sent history
|
||||
this.data.unshift(text);
|
||||
window.sessionStorage.setItem(
|
||||
"history_" + this.roomId,
|
||||
JSON.stringify(this.data)
|
||||
);
|
||||
// reset history position
|
||||
this.position = -1;
|
||||
this.originalText = null;
|
||||
},
|
||||
|
||||
// move in the history. Returns true if we managed to move.
|
||||
next: function(offset) {
|
||||
if (this.position === -1) {
|
||||
// user is going into the history, save the current line.
|
||||
this.originalText = this.element.value;
|
||||
}
|
||||
else {
|
||||
// user may have modified this line in the history; remember it.
|
||||
this.data[this.position] = this.element.value;
|
||||
}
|
||||
|
||||
if (offset > 0 && this.position === (this.data.length - 1)) {
|
||||
// we've run out of history
|
||||
return false;
|
||||
}
|
||||
|
||||
// retrieve the next item (bounded).
|
||||
var newPosition = this.position + offset;
|
||||
newPosition = Math.max(-1, newPosition);
|
||||
newPosition = Math.min(newPosition, this.data.length - 1);
|
||||
this.position = newPosition;
|
||||
|
||||
if (this.position !== -1) {
|
||||
// show the message
|
||||
this.element.value = this.data[this.position];
|
||||
}
|
||||
else if (this.originalText !== undefined) {
|
||||
// restore the original text the user was typing.
|
||||
this.element.value = this.originalText;
|
||||
}
|
||||
|
||||
self.resizeInput();
|
||||
return true;
|
||||
},
|
||||
|
||||
saveLastTextEntry: function() {
|
||||
// save the currently entered text in order to restore it later.
|
||||
// NB: This isn't 'originalText' because we want to restore
|
||||
// sent history items too!
|
||||
var text = this.element.value;
|
||||
window.sessionStorage.setItem("input_" + this.roomId, text);
|
||||
},
|
||||
|
||||
setLastTextEntry: function() {
|
||||
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
||||
if (text) {
|
||||
this.element.value = text;
|
||||
self.resizeInput();
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.sentHistory.init(
|
||||
this.refs.textarea,
|
||||
this.props.room.roomId
|
||||
);
|
||||
this.resizeInput();
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.sentHistory.saveLastTextEntry();
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
var textarea = this.refs.textarea;
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
textarea.focus();
|
||||
break;
|
||||
case 'insert_displayname':
|
||||
if (textarea.value.length) {
|
||||
var left = textarea.value.substring(0, textarea.selectionStart);
|
||||
var right = textarea.value.substring(textarea.selectionEnd);
|
||||
if (right.length) {
|
||||
left += payload.displayname;
|
||||
}
|
||||
else {
|
||||
left = left.replace(/( ?)$/, " " + payload.displayname);
|
||||
}
|
||||
textarea.value = left + right;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(left.length, left.length);
|
||||
}
|
||||
else {
|
||||
textarea.value = payload.displayname + ": ";
|
||||
textarea.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
||||
var input = this.refs.textarea.value;
|
||||
if (input.length === 0) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||
// Remember the keyCode because React will recycle the synthetic event
|
||||
var keyCode = ev.keyCode;
|
||||
// set a callback so we can see if the cursor position changes as
|
||||
// a result of this event. If it doesn't, we cycle history.
|
||||
setTimeout(() => {
|
||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||
this.resizeInput();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.onKeyDown(ev);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (self.refs.textarea && self.refs.textarea.value != '') {
|
||||
self.onTypingActivity();
|
||||
} else {
|
||||
self.onFinishedTyping();
|
||||
}
|
||||
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
|
||||
},
|
||||
|
||||
resizeInput: function() {
|
||||
// scrollHeight is at least equal to clientHeight, so we have to
|
||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
|
||||
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||
this.constructor.MAX_HEIGHT);
|
||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||
|
||||
if (this.props.onResize) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.onResize();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
|
||||
ev.keyCode === KeyCode.DELETE ||
|
||||
ev.keyCode === KeyCode.BACKSPACE)
|
||||
{
|
||||
this.resizeInput();
|
||||
}
|
||||
},
|
||||
|
||||
onEnter: function(ev) {
|
||||
var contentText = this.refs.textarea.value;
|
||||
|
||||
// bodge for now to set markdown state on/off. We probably want a separate
|
||||
// area for "local" commands which don't hit out to the server.
|
||||
if (contentText.indexOf("/markdown") === 0) {
|
||||
ev.preventDefault();
|
||||
this.refs.textarea.value = '';
|
||||
if (contentText.indexOf("/markdown on") === 0) {
|
||||
this.markdownEnabled = true;
|
||||
}
|
||||
else if (contentText.indexOf("/markdown off") === 0) {
|
||||
this.markdownEnabled = false;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Unknown command"),
|
||||
description: _t("Usage") + ": /markdown on|off",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||
if (cmd) {
|
||||
ev.preventDefault();
|
||||
if (!cmd.error) {
|
||||
this.refs.textarea.value = '';
|
||||
}
|
||||
if (cmd.promise) {
|
||||
cmd.promise.done(function() {
|
||||
console.log("Command success.");
|
||||
}, function(err) {
|
||||
console.error("Command failure: %s", err);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Server error"),
|
||||
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (cmd.error) {
|
||||
console.error(cmd.error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Command error"),
|
||||
description: cmd.error,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var isEmote = /^\/me( |$)/i.test(contentText);
|
||||
var sendMessagePromise;
|
||||
|
||||
if (isEmote) {
|
||||
contentText = contentText.substring(4);
|
||||
}
|
||||
else if (contentText[0] === '/') {
|
||||
contentText = contentText.substring(1);
|
||||
}
|
||||
|
||||
let send_markdown = false;
|
||||
let mdown;
|
||||
if (this.markdownEnabled) {
|
||||
mdown = new Markdown(contentText);
|
||||
send_markdown = !mdown.isPlainText();
|
||||
}
|
||||
|
||||
if (send_markdown) {
|
||||
const htmlText = mdown.toHTML();
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
|
||||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||
}
|
||||
else {
|
||||
if (mdown) contentText = mdown.toPlaintext();
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.done(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
}, (e) => onSendMessageFailed(e, this.props.room));
|
||||
|
||||
this.refs.textarea.value = '';
|
||||
this.resizeInput();
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
onTypingActivity: function() {
|
||||
this.isTyping = true;
|
||||
if (!this.userTypingTimer) {
|
||||
this.sendTyping(true);
|
||||
}
|
||||
this.startUserTypingTimer();
|
||||
this.startServerTypingTimer();
|
||||
},
|
||||
|
||||
onFinishedTyping: function() {
|
||||
this.isTyping = false;
|
||||
this.sendTyping(false);
|
||||
this.stopUserTypingTimer();
|
||||
this.stopServerTypingTimer();
|
||||
},
|
||||
|
||||
startUserTypingTimer: function() {
|
||||
this.stopUserTypingTimer();
|
||||
var self = this;
|
||||
this.userTypingTimer = setTimeout(function() {
|
||||
self.isTyping = false;
|
||||
self.sendTyping(self.isTyping);
|
||||
self.userTypingTimer = null;
|
||||
}, TYPING_USER_TIMEOUT);
|
||||
},
|
||||
|
||||
stopUserTypingTimer: function() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this.userTypingTimer);
|
||||
this.userTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
startServerTypingTimer: function() {
|
||||
if (!this.serverTypingTimer) {
|
||||
var self = this;
|
||||
this.serverTypingTimer = setTimeout(function() {
|
||||
if (self.isTyping) {
|
||||
self.sendTyping(self.isTyping);
|
||||
self.startServerTypingTimer();
|
||||
}
|
||||
}, TYPING_SERVER_TIMEOUT / 2);
|
||||
}
|
||||
},
|
||||
|
||||
stopServerTypingTimer: function() {
|
||||
if (this.serverTypingTimer) {
|
||||
clearTimeout(this.servrTypingTimer);
|
||||
this.serverTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
sendTyping: function(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
).done();
|
||||
},
|
||||
|
||||
refreshTyping: function() {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
this.typingTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
onInputClick: function(ev) {
|
||||
this.refs.textarea.focus();
|
||||
},
|
||||
|
||||
_onPaste: function(ev) {
|
||||
const items = ev.clipboardData.items;
|
||||
const files = [];
|
||||
for (const item of items) {
|
||||
if (item.kind === 'file') {
|
||||
files.push(item.getAsFile());
|
||||
}
|
||||
}
|
||||
if (files.length && this.props.onFilesPasted) {
|
||||
this.props.onFilesPasted(files);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||
<textarea dir="auto" autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
|
||||
onPaste={this._onPaste}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
@ -66,6 +66,9 @@ module.exports = React.createClass({
|
||||
|
||||
// Timestamp when the receipt was read
|
||||
timestamp: React.PropTypes.number,
|
||||
|
||||
// True to show twelve hour format, false otherwise
|
||||
showTwelveHour: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -172,7 +175,7 @@ module.exports = React.createClass({
|
||||
if (this.props.timestamp) {
|
||||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp))}
|
||||
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -16,18 +16,18 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require('../../../index');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require("../../../Modal");
|
||||
var dis = require("../../../dispatcher");
|
||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from "../../../Modal";
|
||||
import dis from "../../../dispatcher";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
|
||||
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
|
||||
// When a room name occurs, RoomState.events is fired *before*
|
||||
@ -79,14 +79,14 @@ module.exports = React.createClass({
|
||||
if (this.props.room) {
|
||||
this.props.room.removeListener("Room.name", this._onRoomNameChange);
|
||||
}
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomStateEvents: function(event, state) {
|
||||
if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -94,7 +94,8 @@ module.exports = React.createClass({
|
||||
this._rateLimitedUpdate();
|
||||
},
|
||||
|
||||
_rateLimitedUpdate: new rate_limited_func(function() {
|
||||
_rateLimitedUpdate: new RateLimitedFunc(function() {
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
this.forceUpdate();
|
||||
}, 500),
|
||||
|
||||
@ -109,15 +110,14 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
var self = this;
|
||||
var changeAvatar = this.refs.changeAvatar;
|
||||
const changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
}
|
||||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set avatar: " + errMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
@ -133,10 +133,10 @@ module.exports = React.createClass({
|
||||
/**
|
||||
* After editing the settings, get the new name for the room
|
||||
*
|
||||
* Returns undefined if we didn't let the user edit the room name
|
||||
* @return {?string} newName or undefined if we didn't let the user edit the room name
|
||||
*/
|
||||
getEditedName: function() {
|
||||
var newName;
|
||||
let newName;
|
||||
if (this.refs.nameEditor) {
|
||||
newName = this.refs.nameEditor.getRoomName();
|
||||
}
|
||||
@ -146,10 +146,10 @@ module.exports = React.createClass({
|
||||
/**
|
||||
* After editing the settings, get the new topic for the room
|
||||
*
|
||||
* Returns undefined if we didn't let the user edit the room topic
|
||||
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
|
||||
*/
|
||||
getEditedTopic: function() {
|
||||
var newTopic;
|
||||
let newTopic;
|
||||
if (this.refs.topicEditor) {
|
||||
newTopic = this.refs.topicEditor.getTopic();
|
||||
}
|
||||
@ -157,38 +157,31 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
var header;
|
||||
var name = null;
|
||||
var searchStatus = null;
|
||||
var topic_el = null;
|
||||
var cancel_button = null;
|
||||
var spinner = null;
|
||||
var save_button = null;
|
||||
var settings_button = null;
|
||||
let name = null;
|
||||
let searchStatus = null;
|
||||
let topicElement = null;
|
||||
let cancelButton = null;
|
||||
let spinner = null;
|
||||
let saveButton = null;
|
||||
let settingsButton = null;
|
||||
|
||||
let canSetRoomName;
|
||||
let canSetRoomAvatar;
|
||||
let canSetRoomTopic;
|
||||
if (this.props.editing) {
|
||||
|
||||
// calculate permissions. XXX: this should be done on mount or something
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
const userId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
|
||||
'm.room.name', user_id
|
||||
);
|
||||
var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
|
||||
'm.room.avatar', user_id
|
||||
);
|
||||
var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
|
||||
'm.room.topic', user_id
|
||||
);
|
||||
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
|
||||
'm.room.name', user_id
|
||||
);
|
||||
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
|
||||
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
|
||||
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
|
||||
|
||||
save_button = (
|
||||
saveButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
@ -196,39 +189,41 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
||||
}
|
||||
|
||||
if (can_set_room_name) {
|
||||
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
if (canSetRoomName) {
|
||||
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||
}
|
||||
else {
|
||||
var searchStatus;
|
||||
} else {
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
var settingsHint = false;
|
||||
var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!name || !name.getContent().name) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var roomName = _t("Join Room");
|
||||
let roomName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
roomName = this.props.oobData.name;
|
||||
} else if (this.props.room) {
|
||||
@ -243,24 +238,25 @@ module.exports = React.createClass({
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (can_set_room_topic) {
|
||||
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||
if (canSetRoomTopic) {
|
||||
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||
} else {
|
||||
var topic;
|
||||
let topic;
|
||||
if (this.props.room) {
|
||||
var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (ev) {
|
||||
topic = ev.getContent().topic;
|
||||
}
|
||||
}
|
||||
if (topic) {
|
||||
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
||||
topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
var roomAvatar = null;
|
||||
if (can_set_room_avatar) {
|
||||
let roomAvatar = null;
|
||||
if (canSetRoomAvatar) {
|
||||
roomAvatar = (
|
||||
<div className="mx_RoomHeader_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
@ -276,8 +272,7 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
||||
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
||||
roomAvatar = (
|
||||
<div onClick={this.props.onSettingsClick}>
|
||||
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
|
||||
@ -285,9 +280,8 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
settings_button =
|
||||
settingsButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</AccessibleButton>;
|
||||
@ -301,61 +295,58 @@ module.exports = React.createClass({
|
||||
// </div>;
|
||||
// }
|
||||
|
||||
var forget_button;
|
||||
let forgetButton;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
forgetButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let search_button;
|
||||
let searchButton;
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
search_button =
|
||||
searchButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var rightPanel_buttons;
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanel_buttons =
|
||||
rightPanelButtons =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var right_row;
|
||||
let rightRow;
|
||||
if (!this.props.editing) {
|
||||
right_row =
|
||||
rightRow =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settings_button }
|
||||
{ forget_button }
|
||||
{ search_button }
|
||||
{ rightPanel_buttons }
|
||||
{ settingsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
{ rightPanelButtons }
|
||||
</div>;
|
||||
}
|
||||
|
||||
header =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_leftRow">
|
||||
<div className="mx_RoomHeader_avatar">
|
||||
{ roomAvatar }
|
||||
</div>
|
||||
<div className="mx_RoomHeader_info">
|
||||
{ name }
|
||||
{ topic_el }
|
||||
</div>
|
||||
</div>
|
||||
{spinner}
|
||||
{save_button}
|
||||
{cancel_button}
|
||||
{right_row}
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||
{ header }
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_leftRow">
|
||||
<div className="mx_RoomHeader_avatar">
|
||||
{ roomAvatar }
|
||||
</div>
|
||||
<div className="mx_RoomHeader_info">
|
||||
{ name }
|
||||
{ topicElement }
|
||||
</div>
|
||||
</div>
|
||||
{spinner}
|
||||
{saveButton}
|
||||
{cancelButton}
|
||||
{rightRow}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
@ -33,11 +33,28 @@ var Receipt = require('../../../utils/Receipt');
|
||||
|
||||
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',
|
||||
function phraseForSection(section) {
|
||||
// These would probably be better as individual strings,
|
||||
// but for some reason we have translations for these strings
|
||||
// as-is, so keeping it like this for now.
|
||||
let verb;
|
||||
switch (section) {
|
||||
case 'm.favourite':
|
||||
verb = _t('to favourite');
|
||||
break;
|
||||
case 'im.vector.fake.direct':
|
||||
verb = _t('to tag direct chat');
|
||||
break;
|
||||
case 'im.vector.fake.recent':
|
||||
verb = _t('to restore');
|
||||
break;
|
||||
case 'm.lowpriority':
|
||||
verb = _t('to demote');
|
||||
break;
|
||||
default:
|
||||
return _t('Drop here to tag %(section)s', {section: section});
|
||||
}
|
||||
return _t('Drop here %(toAction)s', {toAction: verb});
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -478,17 +495,25 @@ module.exports = React.createClass({
|
||||
switch (section) {
|
||||
case 'im.vector.fake.direct':
|
||||
return <div className="mx_RoomList_emptySubListTip">
|
||||
Press
|
||||
<StartChatButton size="16" callout={true}/>
|
||||
to start a chat with someone
|
||||
{_tJsx(
|
||||
"Press <StartChatButton> to start a chat with someone",
|
||||
[/<StartChatButton>/],
|
||||
[
|
||||
(sub) => <StartChatButton size="16" callout={true}/>
|
||||
]
|
||||
)}
|
||||
</div>;
|
||||
case 'im.vector.fake.recent':
|
||||
return <div className="mx_RoomList_emptySubListTip">
|
||||
You're not in any rooms yet! Press
|
||||
<CreateRoomButton size="16" callout={true}/>
|
||||
to make a room or
|
||||
<RoomDirectoryButton size="16" callout={true}/>
|
||||
to browse the directory
|
||||
{_tJsx(
|
||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
|
||||
" <RoomDirectoryButton> to browse the directory",
|
||||
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
|
||||
[
|
||||
(sub) => <CreateRoomButton size="16" callout={true}/>,
|
||||
(sub) => <RoomDirectoryButton size="16" callout={true}/>
|
||||
]
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@ -497,7 +522,7 @@ module.exports = React.createClass({
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
|
||||
const labelText = phraseForSection(section);
|
||||
|
||||
return <RoomDropTarget label={labelText} />;
|
||||
},
|
||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) {
|
||||
|
||||
const BannedUser = React.createClass({
|
||||
propTypes: {
|
||||
canUnban: React.PropTypes.bool,
|
||||
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
|
||||
reason: React.PropTypes.string,
|
||||
},
|
||||
@ -67,13 +68,17 @@ const BannedUser = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let unbanButton;
|
||||
|
||||
if (this.props.canUnban) {
|
||||
unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
|
||||
{ _t('Unban') }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<AccessibleButton className="mx_RoomSettings_unbanButton"
|
||||
onClick={this._onUnbanClick}
|
||||
>
|
||||
{ _t('Unban') }
|
||||
</AccessibleButton>
|
||||
{ unbanButton }
|
||||
<strong>{this.props.member.name}</strong> {this.props.member.userId}
|
||||
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
|
||||
</li>
|
||||
@ -178,8 +183,14 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves once all of the save operations have completed or failed.
|
||||
*
|
||||
* The result is a list of promise state snapshots, each with the form
|
||||
* `{ state: "fulfilled", value: v }` or `{ state: "rejected", reason: r }`.
|
||||
*/
|
||||
save: function() {
|
||||
var stateWasSetDefer = q.defer();
|
||||
var stateWasSetDefer = Promise.defer();
|
||||
// the caller may have JUST called setState on stuff, so we need to re-render before saving
|
||||
// else we won't use the latest values of things.
|
||||
// We can be a bit cheeky here and set a loading flag, and listen for the callback on that
|
||||
@ -189,8 +200,18 @@ module.exports = React.createClass({
|
||||
this.setState({ _loading: false});
|
||||
});
|
||||
|
||||
function mapPromiseToSnapshot(p) {
|
||||
return p.then((r) => {
|
||||
return { state: "fulfilled", value: r };
|
||||
}, (e) => {
|
||||
return { state: "rejected", reason: e };
|
||||
});
|
||||
}
|
||||
|
||||
return stateWasSetDefer.promise.then(() => {
|
||||
return q.allSettled(this._calcSavePromises());
|
||||
return Promise.all(
|
||||
this._calcSavePromises().map(mapPromiseToSnapshot),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@ -277,7 +298,7 @@ module.exports = React.createClass({
|
||||
// color scheme
|
||||
var p;
|
||||
p = this.saveColor();
|
||||
if (!q.isFulfilled(p)) {
|
||||
if (!p.isFulfilled()) {
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
@ -289,7 +310,7 @@ module.exports = React.createClass({
|
||||
|
||||
// encryption
|
||||
p = this.saveEnableEncryption();
|
||||
if (!q.isFulfilled(p)) {
|
||||
if (!p.isFulfilled()) {
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
@ -300,25 +321,25 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
saveAliases: function() {
|
||||
if (!this.refs.alias_settings) { return [q()]; }
|
||||
if (!this.refs.alias_settings) { return [Promise.resolve()]; }
|
||||
return this.refs.alias_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveColor: function() {
|
||||
if (!this.refs.color_settings) { return q(); }
|
||||
if (!this.refs.color_settings) { return Promise.resolve(); }
|
||||
return this.refs.color_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveUrlPreviewSettings: function() {
|
||||
if (!this.refs.url_preview_settings) { return q(); }
|
||||
if (!this.refs.url_preview_settings) { return Promise.resolve(); }
|
||||
return this.refs.url_preview_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveEnableEncryption: function() {
|
||||
if (!this.refs.encrypt) { return q(); }
|
||||
if (!this.refs.encrypt) { return Promise.resolve(); }
|
||||
|
||||
var encrypt = this.refs.encrypt.checked;
|
||||
if (!encrypt) { return q(); }
|
||||
if (!encrypt) { return Promise.resolve(); }
|
||||
|
||||
var roomId = this.props.room.roomId;
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
@ -667,6 +688,7 @@ module.exports = React.createClass({
|
||||
const banned = this.props.room.getMembersWithMembership("ban");
|
||||
let bannedUsersSection;
|
||||
if (banned.length) {
|
||||
const canBanUsers = current_user_level >= ban_level;
|
||||
bannedUsersSection =
|
||||
<div>
|
||||
<h3>{ _t('Banned users') }</h3>
|
||||
@ -674,7 +696,7 @@ module.exports = React.createClass({
|
||||
{banned.map(function(member) {
|
||||
const banEvent = member.events.member.getContent();
|
||||
return (
|
||||
<BannedUser key={member.userId} member={member} reason={banEvent.reason} />
|
||||
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -45,17 +42,10 @@ export default React.createClass({
|
||||
title: React.PropTypes.string,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
icon: React.PropTypes.string,
|
||||
},
|
||||
|
||||
onShowRhsClick: function(ev) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let cancelButton;
|
||||
let icon;
|
||||
@ -70,25 +60,12 @@ export default React.createClass({
|
||||
/>;
|
||||
}
|
||||
|
||||
let showRhsButton;
|
||||
/* // don't bother cluttering things up with this for now.
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
if (this.props.collapsedRhs) {
|
||||
showRhsButton =
|
||||
<div className="mx_RoomHeader_button" style={{ float: 'right' }} onClick={this.onShowRhsClick} title=">">
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||
</div>
|
||||
}
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader" >
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ showRhsButton }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TabCompleteBar',
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_TabCompleteBar">
|
||||
{this.props.tabComplete.peek(6).map((entry, i) => {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""}
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
{entry.getText()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
@ -19,10 +19,10 @@ import { _t } from '../../../languageHandler';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default WithMatrixClient(React.createClass({
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'AddPhoneNumber',
|
||||
|
||||
propTypes: {
|
||||
|
@ -21,7 +21,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require("../../../index");
|
||||
|
||||
import q from 'q';
|
||||
import Promise from 'bluebird';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
@ -161,7 +161,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_optionallySetEmail: function() {
|
||||
const deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
// Ask for an email otherwise the user has no way to reset their password
|
||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
||||
Modal.createDialog(SetEmailDialog, {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user