Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into develop
Conflicts: src/i18n/strings/en_EN.json src/i18n/strings/fr.json src/i18n/strings/ru.json
This commit is contained in:
commit
d443368aa3
154 changed files with 5550 additions and 2763 deletions
2
.babelrc
2
.babelrc
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"presets": ["react", "es2015", "es2016"],
|
"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.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
src/AddThreepid.js
|
|
||||||
src/async-components/views/dialogs/EncryptedEventDialog.js
|
src/async-components/views/dialogs/EncryptedEventDialog.js
|
||||||
src/autocomplete/AutocompleteProvider.js
|
src/autocomplete/AutocompleteProvider.js
|
||||||
src/autocomplete/Autocompleter.js
|
src/autocomplete/Autocompleter.js
|
||||||
|
@ -9,8 +8,6 @@ src/autocomplete/DuckDuckGoProvider.js
|
||||||
src/autocomplete/EmojiProvider.js
|
src/autocomplete/EmojiProvider.js
|
||||||
src/autocomplete/RoomProvider.js
|
src/autocomplete/RoomProvider.js
|
||||||
src/autocomplete/UserProvider.js
|
src/autocomplete/UserProvider.js
|
||||||
src/Avatar.js
|
|
||||||
src/BasePlatform.js
|
|
||||||
src/CallHandler.js
|
src/CallHandler.js
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
src/components/structures/ContextualMenu.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/MessageComposerInputOld.js
|
||||||
src/components/views/rooms/PresenceLabel.js
|
src/components/views/rooms/PresenceLabel.js
|
||||||
src/components/views/rooms/ReadReceiptMarker.js
|
src/components/views/rooms/ReadReceiptMarker.js
|
||||||
src/components/views/rooms/RoomHeader.js
|
|
||||||
src/components/views/rooms/RoomList.js
|
src/components/views/rooms/RoomList.js
|
||||||
src/components/views/rooms/RoomNameEditor.js
|
src/components/views/rooms/RoomNameEditor.js
|
||||||
src/components/views/rooms/RoomPreviewBar.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/DevicesPanel.js
|
||||||
src/components/views/settings/DevicesPanelEntry.js
|
src/components/views/settings/DevicesPanelEntry.js
|
||||||
src/components/views/settings/EnableNotificationsButton.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/ContentMessages.js
|
||||||
src/createRoom.js
|
|
||||||
src/DateUtils.js
|
|
||||||
src/email.js
|
|
||||||
src/Entities.js
|
|
||||||
src/extend.js
|
|
||||||
src/HtmlUtils.js
|
src/HtmlUtils.js
|
||||||
src/ImageUtils.js
|
src/ImageUtils.js
|
||||||
src/Invite.js
|
src/Invite.js
|
||||||
|
@ -135,30 +122,20 @@ src/Markdown.js
|
||||||
src/MatrixClientPeg.js
|
src/MatrixClientPeg.js
|
||||||
src/Modal.js
|
src/Modal.js
|
||||||
src/Notifier.js
|
src/Notifier.js
|
||||||
src/ObjectUtils.js
|
|
||||||
src/PasswordReset.js
|
|
||||||
src/PlatformPeg.js
|
src/PlatformPeg.js
|
||||||
src/Presence.js
|
src/Presence.js
|
||||||
src/ratelimitedfunc.js
|
src/ratelimitedfunc.js
|
||||||
src/Resend.js
|
|
||||||
src/RichText.js
|
src/RichText.js
|
||||||
src/Roles.js
|
src/Roles.js
|
||||||
src/RoomListSorter.js
|
|
||||||
src/RoomNotifs.js
|
|
||||||
src/Rooms.js
|
src/Rooms.js
|
||||||
src/ScalarAuthClient.js
|
src/ScalarAuthClient.js
|
||||||
src/ScalarMessaging.js
|
src/ScalarMessaging.js
|
||||||
src/SdkConfig.js
|
|
||||||
src/Skinner.js
|
|
||||||
src/SlashCommands.js
|
|
||||||
src/stores/LifecycleStore.js
|
|
||||||
src/TabComplete.js
|
src/TabComplete.js
|
||||||
src/TabCompleteEntries.js
|
src/TabCompleteEntries.js
|
||||||
src/TextForEvent.js
|
src/TextForEvent.js
|
||||||
src/Tinter.js
|
src/Tinter.js
|
||||||
src/UiEffects.js
|
src/UiEffects.js
|
||||||
src/Unread.js
|
src/Unread.js
|
||||||
src/UserActivity.js
|
|
||||||
src/utils/DecryptFile.js
|
src/utils/DecryptFile.js
|
||||||
src/utils/DMRoomMap.js
|
src/utils/DMRoomMap.js
|
||||||
src/utils/FormattingUtils.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
|
mkdir node_modules
|
||||||
npm install
|
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
|
rm -r node_modules/matrix-react-sdk
|
||||||
ln -s "$REACT_SDK_DIR" 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
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- node # Latest stable version of nodejs.
|
- node # Latest stable version of nodejs.
|
||||||
|
addons:
|
||||||
|
chrome: stable
|
||||||
install:
|
install:
|
||||||
- npm install
|
- npm install
|
||||||
- (cd node_modules/matrix-js-sdk && npm install)
|
- (cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
export KARMAFLAGS="--no-colors"
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||||
nvm use 4
|
nvm use 6
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@ npm install
|
||||||
(cd node_modules/matrix-js-sdk && npm install)
|
(cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
|
||||||
# run the mocha tests
|
# run the mocha tests
|
||||||
npm run test
|
npm run test -- --no-colors
|
||||||
|
|
||||||
# run eslint
|
# run eslint
|
||||||
npm run lintall -- -f checkstyle -o eslint.xml || true
|
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||||
|
|
|
@ -93,7 +93,18 @@ module.exports = function (config) {
|
||||||
// test results reporter to use
|
// test results reporter to use
|
||||||
// possible values: 'dots', 'progress'
|
// possible values: 'dots', 'progress'
|
||||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||||
reporters: ['progress', 'junit'],
|
reporters: ['logcapture', 'spec', 'junit', 'summary'],
|
||||||
|
|
||||||
|
specReporter: {
|
||||||
|
suppressErrorSummary: false, // do print error summary
|
||||||
|
suppressFailed: false, // do print information about failed tests
|
||||||
|
suppressPassed: false, // do print information about passed tests
|
||||||
|
showSpecTiming: true, // print the time elapsed for each spec
|
||||||
|
},
|
||||||
|
|
||||||
|
client: {
|
||||||
|
captureLogs: true,
|
||||||
|
},
|
||||||
|
|
||||||
// web server port
|
// web server port
|
||||||
port: 9876,
|
port: 9876,
|
||||||
|
@ -104,7 +115,10 @@ module.exports = function (config) {
|
||||||
// level of logging
|
// level of logging
|
||||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
||||||
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||||
logLevel: config.LOG_INFO,
|
//
|
||||||
|
// This is strictly for logs that would be generated by the browser itself and we
|
||||||
|
// don't want to log about missing images, which are emitted on LOG_WARN.
|
||||||
|
logLevel: config.LOG_ERROR,
|
||||||
|
|
||||||
// enable / disable watching file and executing tests whenever any file
|
// enable / disable watching file and executing tests whenever any file
|
||||||
// changes
|
// changes
|
||||||
|
@ -116,11 +130,25 @@ module.exports = function (config) {
|
||||||
browsers: [
|
browsers: [
|
||||||
'Chrome',
|
'Chrome',
|
||||||
//'PhantomJS',
|
//'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
|
// Continuous Integration mode
|
||||||
// if true, Karma captures browsers, runs the tests and exits
|
// if true, Karma captures browsers, runs the tests and exits
|
||||||
singleRun: true,
|
// singleRun: false,
|
||||||
|
|
||||||
// Concurrency level
|
// Concurrency level
|
||||||
// how many browser should be started simultaneous
|
// how many browser should be started simultaneous
|
||||||
|
|
34
package.json
34
package.json
|
@ -33,28 +33,30 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "node scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
"build": "npm run reskindex && babel src -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",
|
"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\"",
|
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start $KARMAFLAGS --browsers PhantomJS",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
"test-multi": "karma start $KARMAFLAGS --single-run=false"
|
"test-multi": "karma start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
|
"bluebird": "^3.5.0",
|
||||||
"blueimp-canvas-to-blob": "^3.5.0",
|
"blueimp-canvas-to-blob": "^3.5.0",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.27.0",
|
"commonmark": "^0.27.0",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"draft-js": "^0.8.1",
|
"draft-js": "^0.11.0-alpha",
|
||||||
"draft-js-export-html": "^0.5.0",
|
"draft-js-export-html": "^0.6.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.3.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.7",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
"flux": "2.1.1",
|
"flux": "2.1.1",
|
||||||
|
@ -64,16 +66,16 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.7.13",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"q": "^1.4.1",
|
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.14.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
|
"url": "^0.11.0",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -83,7 +85,7 @@
|
||||||
"babel-eslint": "^6.1.2",
|
"babel-eslint": "^6.1.2",
|
||||||
"babel-loader": "^6.2.5",
|
"babel-loader": "^6.2.5",
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"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-class-properties": "^6.16.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||||
"babel-plugin-transform-runtime": "^6.15.0",
|
"babel-plugin-transform-runtime": "^6.15.0",
|
||||||
|
@ -100,17 +102,19 @@
|
||||||
"eslint-plugin-react": "^6.9.0",
|
"eslint-plugin-react": "^6.9.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^1.7.0",
|
||||||
"karma-chrome-launcher": "^0.2.3",
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
"karma-cli": "^0.1.2",
|
"karma-cli": "^0.1.2",
|
||||||
"karma-junit-reporter": "^0.4.1",
|
"karma-junit-reporter": "^0.4.1",
|
||||||
|
"karma-logcapture-reporter": "0.0.1",
|
||||||
"karma-mocha": "^0.2.2",
|
"karma-mocha": "^0.2.2",
|
||||||
"karma-phantomjs-launcher": "^1.0.0",
|
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
|
"karma-spec-reporter": "^0.0.31",
|
||||||
|
"karma-summary-reporter": "^1.3.3",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
|
"matrix-react-test-utils": "^0.1.1",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
"parallelshell": "^1.2.0",
|
"parallelshell": "^1.2.0",
|
||||||
"phantomjs-prebuilt": "^2.1.7",
|
|
||||||
"react-addons-test-utils": "^15.4.0",
|
"react-addons-test-utils": "^15.4.0",
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
"rimraf": "^2.4.3",
|
"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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,7 +44,7 @@ class AddThreepid {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, 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');
|
err.message = _t('This email address is already in use');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
|
@ -69,7 +69,7 @@ class AddThreepid {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, 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');
|
err.message = _t('This phone number is already in use');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
|
@ -85,16 +85,15 @@ class AddThreepid {
|
||||||
* the request failed.
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
checkEmailLinkClicked() {
|
checkEmailLinkClicked() {
|
||||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
return MatrixClientPeg.get().addThreePid({
|
return MatrixClientPeg.get().addThreePid({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: identityServerDomain
|
id_server: identityServerDomain,
|
||||||
}, this.bind).catch(function(err) {
|
}, this.bind).catch(function(err) {
|
||||||
if (err.httpStatus === 401) {
|
if (err.httpStatus === 401) {
|
||||||
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
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})`;
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -104,6 +103,7 @@ class AddThreepid {
|
||||||
/**
|
/**
|
||||||
* Takes a phone number verification code as entered by the user and validates
|
* 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.
|
* 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
|
* @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
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the request failed.
|
* the request failed.
|
||||||
|
@ -119,7 +119,7 @@ class AddThreepid {
|
||||||
return MatrixClientPeg.get().addThreePid({
|
return MatrixClientPeg.get().addThreePid({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: identityServerDomain
|
id_server: identityServerDomain,
|
||||||
}, this.bind);
|
}, this.bind);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentLanguage } from './languageHandler';
|
import { getCurrentLanguage } from './languageHandler';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
|
|
||||||
|
@ -31,8 +30,18 @@ const customVariables = {
|
||||||
'User Type': 3,
|
'User Type': 3,
|
||||||
'Chosen Language': 4,
|
'Chosen Language': 4,
|
||||||
'Instance': 5,
|
'Instance': 5,
|
||||||
|
'RTE: Uses Richtext Mode': 6,
|
||||||
|
'Homeserver URL': 7,
|
||||||
|
'Identity Server URL': 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function whitelistRedact(whitelist, str) {
|
||||||
|
if (whitelist.includes(str)) return str;
|
||||||
|
return '<redacted>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const whitelistedHSUrls = ["https://matrix.org"];
|
||||||
|
const whitelistedISUrls = ["https://vector.im"];
|
||||||
|
|
||||||
class Analytics {
|
class Analytics {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -76,7 +85,7 @@ class Analytics {
|
||||||
this._paq.push(['trackAllContentImpressions']);
|
this._paq.push(['trackAllContentImpressions']);
|
||||||
this._paq.push(['discardHashTag', false]);
|
this._paq.push(['discardHashTag', false]);
|
||||||
this._paq.push(['enableHeartBeatTimer']);
|
this._paq.push(['enableHeartBeatTimer']);
|
||||||
this._paq.push(['enableLinkTracking', true]);
|
// this._paq.push(['enableLinkTracking', true]);
|
||||||
|
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||||
|
@ -130,20 +139,20 @@ class Analytics {
|
||||||
this._paq.push(['deleteCookies']);
|
this._paq.push(['deleteCookies']);
|
||||||
}
|
}
|
||||||
|
|
||||||
login() { // not used currently
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (this.disabled || !cli) return;
|
|
||||||
|
|
||||||
this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
_setVisitVariable(key, value) {
|
||||||
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGuest(guest) {
|
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
|
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
|
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
|
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
setRichtextMode(state) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
import {ContentRepo} from 'matrix-js-sdk';
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||||
var url = member.getAvatarUrl(
|
let url = member.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
Math.floor(width * window.devicePixelRatio),
|
Math.floor(width * window.devicePixelRatio),
|
||||||
Math.floor(height * window.devicePixelRatio),
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod,
|
resizeMethod,
|
||||||
false,
|
false,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
// member can be null here currently since on invites, the JS SDK
|
// member can be null here currently since on invites, the JS SDK
|
||||||
|
@ -38,11 +38,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||||
var url = ContentRepo.getHttpUriForMxc(
|
const url = ContentRepo.getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||||
Math.floor(width * window.devicePixelRatio),
|
Math.floor(width * window.devicePixelRatio),
|
||||||
Math.floor(height * window.devicePixelRatio),
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod
|
resizeMethod,
|
||||||
);
|
);
|
||||||
if (!url || url.length === 0) {
|
if (!url || url.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,11 +51,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultAvatarUrlForString: function(s) {
|
defaultAvatarUrlForString: function(s) {
|
||||||
var images = ['76cfa6', '50e2c2', 'f4c371'];
|
const images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||||
var total = 0;
|
let total = 0;
|
||||||
for (var i = 0; i < s.length; ++i) {
|
for (let i = 0; i < s.length; ++i) {
|
||||||
total += s.charCodeAt(i);
|
total += s.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return 'img/' + images[total % images.length] + '.png';
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default class BasePlatform {
|
||||||
/**
|
/**
|
||||||
* Returns true if the platform supports displaying
|
* Returns true if the platform supports displaying
|
||||||
* notifications, otherwise false.
|
* notifications, otherwise false.
|
||||||
|
* @returns {boolean} whether the platform supports displaying notifications
|
||||||
*/
|
*/
|
||||||
supportsNotifications(): boolean {
|
supportsNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
@ -65,6 +66,7 @@ export default class BasePlatform {
|
||||||
/**
|
/**
|
||||||
* Returns true if the application currently has permission
|
* Returns true if the application currently has permission
|
||||||
* to display notifications. Otherwise false.
|
* to display notifications. Otherwise false.
|
||||||
|
* @returns {boolean} whether the application has permission to display notifications
|
||||||
*/
|
*/
|
||||||
maySendNotifications(): boolean {
|
maySendNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -143,7 +143,7 @@ function _setCallListeners(call) {
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
play("busyAudio");
|
play("busyAudio");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||||
title: _t('Call Timeout'),
|
title: _t('Call Timeout'),
|
||||||
description: _t('The remote side failed to pick up') + '.',
|
description: _t('The remote side failed to pick up') + '.',
|
||||||
});
|
});
|
||||||
|
@ -205,7 +205,7 @@ function _onAction(payload) {
|
||||||
_setCallState(undefined, newCall.roomId, "ended");
|
_setCallState(undefined, newCall.roomId, "ended");
|
||||||
console.log("Can't capture screen: " + screenCapErrorString);
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||||
title: _t('Unable to capture screen'),
|
title: _t('Unable to capture screen'),
|
||||||
description: screenCapErrorString,
|
description: screenCapErrorString,
|
||||||
});
|
});
|
||||||
|
@ -225,7 +225,7 @@ function _onAction(payload) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
if (module.exports.getAnyActiveCall()) {
|
if (module.exports.getAnyActiveCall()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
title: _t('Existing Call'),
|
title: _t('Existing Call'),
|
||||||
description: _t('You are already in a call.'),
|
description: _t('You are already in a call.'),
|
||||||
});
|
});
|
||||||
|
@ -235,7 +235,7 @@ function _onAction(payload) {
|
||||||
// if the runtime env doesn't do VoIP, whine.
|
// if the runtime env doesn't do VoIP, whine.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
|
@ -251,7 +251,7 @@ function _onAction(payload) {
|
||||||
var members = room.getJoinedMembers();
|
var members = room.getJoinedMembers();
|
||||||
if (members.length <= 1) {
|
if (members.length <= 1) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||||
description: _t('You cannot place a call with yourself.'),
|
description: _t('You cannot place a call with yourself.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -277,13 +277,13 @@ function _onAction(payload) {
|
||||||
console.log("Place conference call in %s", payload.room_id);
|
console.log("Place conference call in %s", payload.room_id);
|
||||||
if (!ConferenceHandler) {
|
if (!ConferenceHandler) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in this client'),
|
description: _t('Conference calls are not supported in this client'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (!MatrixClientPeg.get().supportsVoip()) {
|
else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
|
@ -296,13 +296,13 @@ function _onAction(payload) {
|
||||||
// participant.
|
// participant.
|
||||||
// Therefore we disable conference calling in E2E rooms.
|
// Therefore we disable conference calling in E2E rooms.
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description: _t('Conference calling is in development and may not be reliable.'),
|
description: _t('Conference calling is in development and may not be reliable.'),
|
||||||
onFinished: confirm=>{
|
onFinished: confirm=>{
|
||||||
|
@ -314,7 +314,7 @@ function _onAction(payload) {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Conference call failed: " + err);
|
console.error("Conference call failed: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
|
||||||
title: _t('Failed to set up conference call'),
|
title: _t('Failed to set up conference call'),
|
||||||
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
||||||
});
|
});
|
||||||
|
|
84
src/ComposerHistoryManager.js
Normal file
84
src/ComposerHistoryManager.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
//@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, convertToRaw, convertFromRaw} from 'draft-js';
|
||||||
|
import * as RichText from './RichText';
|
||||||
|
import Markdown from './Markdown';
|
||||||
|
import _clamp from 'lodash/clamp';
|
||||||
|
|
||||||
|
type MessageFormat = 'html' | 'markdown';
|
||||||
|
|
||||||
|
class HistoryItem {
|
||||||
|
|
||||||
|
// Keeping message for backwards-compatibility
|
||||||
|
message: string;
|
||||||
|
rawContentState: RawDraftContentState;
|
||||||
|
format: MessageFormat = 'html';
|
||||||
|
|
||||||
|
constructor(contentState: ?ContentState, format: ?MessageFormat) {
|
||||||
|
this.rawContentState = contentState ? convertToRaw(contentState) : null;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
toContentState(outputFormat: MessageFormat): ContentState {
|
||||||
|
const contentState = convertFromRaw(this.rawContentState);
|
||||||
|
if (outputFormat === 'markdown') {
|
||||||
|
if (this.format === 'html') {
|
||||||
|
return ContentState.createFromText(RichText.stateToMarkdown(contentState));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.format === 'markdown') {
|
||||||
|
return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// history item has format === outputFormat
|
||||||
|
return contentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(contentState: ContentState, format: MessageFormat) {
|
||||||
|
const item = new HistoryItem(contentState, 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';
|
'use strict';
|
||||||
|
|
||||||
var q = require('q');
|
import Promise from 'bluebird';
|
||||||
var extend = require('./extend');
|
var extend = require('./extend');
|
||||||
var dis = require('./dispatcher');
|
var dis = require('./dispatcher');
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
@ -52,7 +52,7 @@ const MAX_HEIGHT = 600;
|
||||||
* and a thumbnail key.
|
* and a thumbnail key.
|
||||||
*/
|
*/
|
||||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
var targetWidth = inputWidth;
|
var targetWidth = inputWidth;
|
||||||
var targetHeight = inputHeight;
|
var targetHeight = inputHeight;
|
||||||
|
@ -95,7 +95,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
* @return {Promise} A promise that resolves with the html image element.
|
* @return {Promise} A promise that resolves with the html image element.
|
||||||
*/
|
*/
|
||||||
function loadImageElement(imageFile) {
|
function loadImageElement(imageFile) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const img = document.createElement("img");
|
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.
|
* @return {Promise} A promise that resolves with the video image element.
|
||||||
*/
|
*/
|
||||||
function loadVideoElement(videoFile) {
|
function loadVideoElement(videoFile) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
@ -210,7 +210,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
* is read.
|
* is read.
|
||||||
*/
|
*/
|
||||||
function readFileAsArrayBuffer(file) {
|
function readFileAsArrayBuffer(file) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
deferred.resolve(e.target.result);
|
deferred.resolve(e.target.result);
|
||||||
|
@ -229,11 +229,13 @@ function readFileAsArrayBuffer(file) {
|
||||||
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
||||||
* @param {String} roomId The ID of the room being uploaded to.
|
* @param {String} roomId The ID of the room being uploaded to.
|
||||||
* @param {File} file The file to upload.
|
* @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.
|
* @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 unencrypted then the object will have a "url" key.
|
||||||
* If the file is encrypted then the object will have a "file" 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 (matrixClient.isRoomEncrypted(roomId)) {
|
||||||
// If the room is encrypted then encrypt the file before uploading it.
|
// If the room is encrypted then encrypt the file before uploading it.
|
||||||
// First read the file into memory.
|
// First read the file into memory.
|
||||||
|
@ -245,7 +247,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||||
const encryptInfo = encryptResult.info;
|
const encryptInfo = encryptResult.info;
|
||||||
// Pass the encrypted data as a Blob to the uploader.
|
// Pass the encrypted data as a Blob to the uploader.
|
||||||
const blob = new Blob([encryptResult.data]);
|
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
|
// If the attachment is encrypted then bundle the URL along
|
||||||
// with the information needed to decrypt the attachment and
|
// with the information needed to decrypt the attachment and
|
||||||
// add it under a file key.
|
// add it under a file key.
|
||||||
|
@ -257,7 +261,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const basePromise = matrixClient.uploadContent(file);
|
const basePromise = matrixClient.uploadContent(file, {
|
||||||
|
progressHandler: progressHandler,
|
||||||
|
});
|
||||||
const promise1 = basePromise.then(function(url) {
|
const promise1 = basePromise.then(function(url) {
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return {"url": url};
|
return {"url": url};
|
||||||
|
@ -288,7 +294,7 @@ class ContentMessages {
|
||||||
content.info.mimetype = file.type;
|
content.info.mimetype = file.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
const def = q.defer();
|
const def = Promise.defer();
|
||||||
if (file.type.indexOf('image/') == 0) {
|
if (file.type.indexOf('image/') == 0) {
|
||||||
content.msgtype = 'm.image';
|
content.msgtype = 'm.image';
|
||||||
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
||||||
|
@ -326,23 +332,24 @@ class ContentMessages {
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
var error;
|
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() {
|
return def.promise.then(function() {
|
||||||
// XXX: upload.promise must be the promise that
|
// XXX: upload.promise must be the promise that
|
||||||
// is returned by uploadFile as it has an abort()
|
// is returned by uploadFile as it has an abort()
|
||||||
// method hacked onto it.
|
// method hacked onto it.
|
||||||
upload.promise = uploadFile(
|
upload.promise = uploadFile(
|
||||||
matrixClient, roomId, file
|
matrixClient, roomId, file, onProgress,
|
||||||
);
|
);
|
||||||
return upload.promise.then(function(result) {
|
return upload.promise.then(function(result) {
|
||||||
content.file = result.file;
|
content.file = result.file;
|
||||||
content.url = result.url;
|
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) {
|
}).then(function(url) {
|
||||||
return matrixClient.sendMessage(roomId, content);
|
return matrixClient.sendMessage(roomId, content);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
@ -353,7 +360,7 @@ class ContentMessages {
|
||||||
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||||
title: _t('Upload Failed'),
|
title: _t('Upload Failed'),
|
||||||
description: desc,
|
description: desc,
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,24 +54,25 @@ function pad(n) {
|
||||||
function twelveHourTime(date) {
|
function twelveHourTime(date) {
|
||||||
let hours = date.getHours() % 12;
|
let hours = date.getHours() % 12;
|
||||||
const minutes = pad(date.getMinutes());
|
const minutes = pad(date.getMinutes());
|
||||||
const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
|
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||||
hours = pad(hours ? hours : 12);
|
hours = hours ? hours : 12; // convert 0 -> 12
|
||||||
return `${hours}:${minutes}${ampm}`;
|
return `${hours}:${minutes}${ampm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
formatDate: function(date, showTwelveHour=false) {
|
formatDate: function(date, showTwelveHour=false) {
|
||||||
var now = new Date();
|
const now = new Date();
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
return this.formatTime(date);
|
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
|
// TODO: use standard date localize function provided in counterpart
|
||||||
return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date, showTwelveHour)});
|
return _t('%(weekDayName)s %(time)s', {
|
||||||
}
|
weekDayName: days[date.getDay()],
|
||||||
else if (now.getFullYear() === date.getFullYear()) {
|
time: this.formatTime(date, showTwelveHour),
|
||||||
|
});
|
||||||
|
} else if (now.getFullYear() === date.getFullYear()) {
|
||||||
// TODO: use standard date localize function provided in counterpart
|
// TODO: use standard date localize function provided in counterpart
|
||||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
|
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
|
||||||
weekDayName: days[date.getDay()],
|
weekDayName: days[date.getDay()],
|
||||||
|
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import sdk from './index';
|
||||||
var sdk = require('./index');
|
|
||||||
|
|
||||||
function isMatch(query, name, uid) {
|
function isMatch(query, name, uid) {
|
||||||
query = query.toLowerCase();
|
query = query.toLowerCase();
|
||||||
|
@ -33,8 +32,8 @@ function isMatch(query, name, uid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// split spaces in name and try matching constituent parts
|
// split spaces in name and try matching constituent parts
|
||||||
var parts = name.split(" ");
|
const parts = name.split(" ");
|
||||||
for (var i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (parts[i].indexOf(query) === 0) {
|
if (parts[i].indexOf(query) === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -67,7 +66,7 @@ class Entity {
|
||||||
|
|
||||||
class MemberEntity extends Entity {
|
class MemberEntity extends Entity {
|
||||||
getJsx() {
|
getJsx() {
|
||||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
return (
|
return (
|
||||||
<MemberTile key={this.model.userId} member={this.model} />
|
<MemberTile key={this.model.userId} member={this.model} />
|
||||||
);
|
);
|
||||||
|
@ -84,6 +83,7 @@ class UserEntity extends Entity {
|
||||||
super(model);
|
super(model);
|
||||||
this.showInviteButton = Boolean(showInviteButton);
|
this.showInviteButton = Boolean(showInviteButton);
|
||||||
this.inviteFn = inviteFn;
|
this.inviteFn = inviteFn;
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -93,15 +93,15 @@ class UserEntity extends Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
getJsx() {
|
getJsx() {
|
||||||
var UserTile = sdk.getComponent("rooms.UserTile");
|
const UserTile = sdk.getComponent("rooms.UserTile");
|
||||||
return (
|
return (
|
||||||
<UserTile key={this.model.userId} user={this.model}
|
<UserTile key={this.model.userId} user={this.model}
|
||||||
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
|
showInviteButton={this.showInviteButton} onClick={this.onClick} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(queryString) {
|
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);
|
return isMatch(queryString, name, this.model.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ class UserEntity extends Entity {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
newEntity: function(jsx, matchFn) {
|
newEntity: function(jsx, matchFn) {
|
||||||
var entity = new Entity();
|
const entity = new Entity();
|
||||||
entity.getJsx = function() {
|
entity.getJsx = function() {
|
||||||
return jsx;
|
return jsx;
|
||||||
};
|
};
|
||||||
|
@ -137,5 +137,5 @@ module.exports = {
|
||||||
return users.map(function(u) {
|
return users.map(function(u) {
|
||||||
return new UserEntity(u, showInviteButton, inviteFn);
|
return new UserEntity(u, showInviteButton, inviteFn);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,7 @@ var linkifyMatrix = require('./linkify-matrix');
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
import emojione from 'emojione';
|
import emojione from 'emojione';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
emojione.imagePathSVG = 'emojione/svg/';
|
emojione.imagePathSVG = 'emojione/svg/';
|
||||||
// Store PNG path for displaying many flags at once (for increased performance over 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
|
* because we want to include emoji shortnames in title text
|
||||||
*/
|
*/
|
||||||
export function unicodeToImage(str) {
|
export function unicodeToImage(str) {
|
||||||
let replaceWith, unicode, alt;
|
let replaceWith, unicode, alt, short, fname;
|
||||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
|
||||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||||
|
@ -49,11 +50,14 @@ export function unicodeToImage(str) {
|
||||||
// get the unicode codepoint from the actual char
|
// get the unicode codepoint from the actual char
|
||||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
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
|
// 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];
|
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||||
const title = 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;
|
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');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
|
||||||
|
@ -96,7 +100,18 @@ export function stripParagraphs(html: string): string {
|
||||||
for (let i=0; i < contentDiv.children.length; i++) {
|
for (let i=0; i < contentDiv.children.length; i++) {
|
||||||
const element = contentDiv.children[i];
|
const element = contentDiv.children[i];
|
||||||
if (element.tagName.toLowerCase() === 'p') {
|
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 {
|
} else {
|
||||||
const temp = document.createElement('div');
|
const temp = document.createElement('div');
|
||||||
temp.appendChild(element.cloneNode(true));
|
temp.appendChild(element.cloneNode(true));
|
||||||
|
@ -107,33 +122,39 @@ export function stripParagraphs(html: string): string {
|
||||||
return contentHTML;
|
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: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
'del', // for markdown
|
'del', // for markdown
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
'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: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// custom ones first:
|
||||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
// We don't currently allow img itself by default, but this
|
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||||
// would make sense if we did
|
|
||||||
img: ['src'],
|
|
||||||
ol: ['start'],
|
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
|
// Lots of these won't come up by default because we don't allow them
|
||||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||||
// URL schemes we permit
|
// URL schemes we permit
|
||||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
||||||
|
|
||||||
// DO NOT USE. sanitize-html allows all URL starting with '//'
|
allowProtocolRelative: false,
|
||||||
// so this will always allow links to whatever scheme the
|
|
||||||
// host page is served over.
|
|
||||||
allowedSchemesByTag: {},
|
|
||||||
|
|
||||||
transformTags: { // custom to matrix
|
transformTags: { // custom to matrix
|
||||||
// add blank targets to all hyperlinks except vector URLs
|
// add blank targets to all hyperlinks except vector URLs
|
||||||
|
@ -165,6 +186,33 @@ var sanitizeHtmlParams = {
|
||||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName: tagName, attribs : attribs };
|
return { tagName: tagName, attribs : attribs };
|
||||||
},
|
},
|
||||||
|
'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) {
|
'*': function(tagName, attribs) {
|
||||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||||
// because attributes are stripped after transforming
|
// because attributes are stripped after transforming
|
||||||
|
|
|
@ -21,6 +21,7 @@ module.exports = {
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
SHIFT: 16,
|
SHIFT: 16,
|
||||||
ESCAPE: 27,
|
ESCAPE: 27,
|
||||||
|
SPACE: 32,
|
||||||
PAGE_UP: 33,
|
PAGE_UP: 33,
|
||||||
PAGE_DOWN: 34,
|
PAGE_DOWN: 34,
|
||||||
END: 35,
|
END: 35,
|
||||||
|
@ -30,7 +31,30 @@ module.exports = {
|
||||||
RIGHT: 39,
|
RIGHT: 39,
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
DELETE: 46,
|
DELETE: 46,
|
||||||
|
KEY_A: 65,
|
||||||
|
KEY_B: 66,
|
||||||
|
KEY_C: 67,
|
||||||
KEY_D: 68,
|
KEY_D: 68,
|
||||||
KEY_E: 69,
|
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_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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default class KeyRequestHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
||||||
Modal.createDialog(KeyShareDialog, {
|
Modal.createTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
|
||||||
matrixClient: this._matrixClient,
|
matrixClient: this._matrixClient,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
@ -116,12 +116,12 @@ export function loadSession(opts) {
|
||||||
*/
|
*/
|
||||||
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||||
if (!queryParams.loginToken) {
|
if (!queryParams.loginToken) {
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queryParams.homeserver) {
|
if (!queryParams.homeserver) {
|
||||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a temporary MatrixClient to do the login
|
// create a temporary MatrixClient to do the login
|
||||||
|
@ -197,7 +197,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
// localStorage (e.g. teamToken, isGuest etc.)
|
// localStorage (e.g. teamToken, isGuest etc.)
|
||||||
function _restoreFromLocalStorage() {
|
function _restoreFromLocalStorage() {
|
||||||
if (!localStorage) {
|
if (!localStorage) {
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||||
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||||
|
@ -229,18 +229,18 @@ function _restoreFromLocalStorage() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("No previous session found.");
|
console.log("No previous session found.");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleRestoreFailure(e) {
|
function _handleRestoreFailure(e) {
|
||||||
console.log("Unable to restore session", e);
|
console.log("Unable to restore session", e);
|
||||||
|
|
||||||
const def = q.defer();
|
const def = Promise.defer();
|
||||||
const SessionRestoreErrorDialog =
|
const SessionRestoreErrorDialog =
|
||||||
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
||||||
|
|
||||||
Modal.createDialog(SessionRestoreErrorDialog, {
|
Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
|
||||||
error: e.message,
|
error: e.message,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
def.resolve(success);
|
def.resolve(success);
|
||||||
|
@ -309,13 +309,16 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
// because `teamPromise` may take some time to resolve, breaking the assumption that
|
// 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
|
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
|
||||||
// later than MatrixChat might assume.
|
// 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) {
|
if (clearStorage) {
|
||||||
await _clearStorage();
|
await _clearStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.setGuest(credentials.guest);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
|
||||||
|
|
||||||
// Resolves by default
|
// Resolves by default
|
||||||
let teamPromise = Promise.resolve(null);
|
let teamPromise = Promise.resolve(null);
|
||||||
|
@ -344,6 +347,9 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
localStorage.setItem("mx_team_token", body.team_token);
|
localStorage.setItem("mx_team_token", body.team_token);
|
||||||
}
|
}
|
||||||
return body.team_token;
|
return body.team_token;
|
||||||
|
}, (err) => {
|
||||||
|
console.warn(`Failed to get team token on login: ${err}` );
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -354,9 +360,6 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
|
|
||||||
teamPromise.then((teamToken) => {
|
teamPromise.then((teamToken) => {
|
||||||
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||||
}, (err) => {
|
|
||||||
console.warn("Failed to get team token on login", err);
|
|
||||||
dis.dispatch({action: 'on_logged_in', teamToken: null});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
startMatrixClient();
|
startMatrixClient();
|
||||||
|
@ -419,6 +422,8 @@ export function logout() {
|
||||||
* listen for events while a session is logged in.
|
* listen for events while a session is logged in.
|
||||||
*/
|
*/
|
||||||
function startMatrixClient() {
|
function startMatrixClient() {
|
||||||
|
console.log(`Lifecycle: Starting MatrixClient`);
|
||||||
|
|
||||||
// dispatch this before starting the matrix client: it's used
|
// dispatch this before starting the matrix client: it's used
|
||||||
// to add listeners for the 'sync' event so otherwise we'd have
|
// to add listeners for the 'sync' event so otherwise we'd have
|
||||||
// a race condition (and we need to dispatch synchronously for this
|
// a race condition (and we need to dispatch synchronously for this
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
import { _t } from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
export default class Login {
|
export default class Login {
|
||||||
|
@ -144,7 +144,7 @@ export default class Login {
|
||||||
|
|
||||||
const client = this._createTemporaryClient();
|
const client = this._createTemporaryClient();
|
||||||
return client.login('m.login.password', loginParams).then(function(data) {
|
return client.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
homeserverUrl: self._hsUrl,
|
homeserverUrl: self._hsUrl,
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
|
@ -160,7 +160,7 @@ export default class Login {
|
||||||
});
|
});
|
||||||
|
|
||||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
homeserverUrl: self._fallbackHsUrl,
|
homeserverUrl: self._fallbackHsUrl,
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import commonmark from 'commonmark';
|
import commonmark from 'commonmark';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['del'];
|
const ALLOWED_HTML_TAGS = ['del', 'u'];
|
||||||
|
|
||||||
// These types of node are definitely text
|
// These types of node are definitely text
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
@ -55,6 +55,25 @@ function is_multi_line(node) {
|
||||||
return par.firstChild != par.lastChild;
|
return par.firstChild != par.lastChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import linkifyMatrix from './linkify-matrix';
|
||||||
|
import * as linkify from 'linkifyjs';
|
||||||
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
// Thieved from draft-js-export-markdown
|
||||||
|
function escapeMarkdown(s) {
|
||||||
|
return s.replace(/[*_`]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace URLs, room aliases and user IDs with md-escaped URLs
|
||||||
|
function linkifyMarkdown(s) {
|
||||||
|
const links = linkify.find(s);
|
||||||
|
links.forEach((l) => {
|
||||||
|
// This may replace several instances of `l.value` at once, but that's OK
|
||||||
|
s = s.replace(l.value, escapeMarkdown(l.value));
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that wraps commonmark, adding the ability to see whether
|
* Class that wraps commonmark, adding the ability to see whether
|
||||||
* a given message actually uses any markdown syntax or whether
|
* a given message actually uses any markdown syntax or whether
|
||||||
|
@ -62,7 +81,7 @@ function is_multi_line(node) {
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = linkifyMarkdown(input);
|
||||||
|
|
||||||
const parser = new commonmark.Parser();
|
const parser = new commonmark.Parser();
|
||||||
this.parsed = parser.parse(this.input);
|
this.parsed = parser.parse(this.input);
|
||||||
|
|
|
@ -77,22 +77,38 @@ class MatrixClientPeg {
|
||||||
this._createClient(creds);
|
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);
|
const opts = utils.deepCopy(this.opts);
|
||||||
// the react sdk doesn't work without this, so don't allow
|
// the react sdk doesn't work without this, so don't allow
|
||||||
opts.pendingEventOrdering = "detached";
|
opts.pendingEventOrdering = "detached";
|
||||||
|
|
||||||
|
try {
|
||||||
let promise = this.matrixClient.store.startup();
|
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)
|
// log any errors when starting up the database (if one exists)
|
||||||
promise.catch((err) => {
|
|
||||||
console.error(`Error starting matrixclient store: ${err}`);
|
console.error(`Error starting matrixclient store: ${err}`);
|
||||||
});
|
}
|
||||||
|
|
||||||
// regardless of errors, start the client. If we did error out, we'll
|
// regardless of errors, start the client. If we did error out, we'll
|
||||||
// just end up doing a full initial /sync.
|
// just end up doing a full initial /sync.
|
||||||
promise.finally(() => {
|
|
||||||
|
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||||
this.get().startClient(opts);
|
this.get().startClient(opts);
|
||||||
});
|
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(): MatrixClientCreds {
|
getCredentials(): MatrixClientCreds {
|
||||||
|
|
13
src/Modal.js
13
src/Modal.js
|
@ -103,13 +103,20 @@ class ModalManager {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
createDialog(Element, props, className) {
|
createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) {
|
||||||
if (props && props.title) {
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
Analytics.trackEvent('Modal', props.title, 'createDialog');
|
return this.createDialog(Element, props, className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createDialog(Element, props, className) {
|
||||||
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTrackedDialogAsync(analyticsId, loader, props, className) {
|
||||||
|
Analytics.trackEvent('Modal', analyticsId);
|
||||||
|
return this.createDialogAsync(loader, props, className);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a modal view.
|
* Open a modal view.
|
||||||
*
|
*
|
||||||
|
|
|
@ -142,7 +142,7 @@ const Notifier = {
|
||||||
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
||||||
: _t('Riot was not given permission to send notifications - please try again');
|
: _t('Riot was not given permission to send notifications - please try again');
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, {
|
||||||
title: _t('Unable to enable Notifications'),
|
title: _t('Unable to enable Notifications'),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,8 +23,8 @@ limitations under the License.
|
||||||
* { key: $KEY, val: $VALUE, place: "add|del" }
|
* { key: $KEY, val: $VALUE, place: "add|del" }
|
||||||
*/
|
*/
|
||||||
module.exports.getKeyValueArrayDiffs = function(before, after) {
|
module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
var results = [];
|
const results = [];
|
||||||
var delta = {};
|
const delta = {};
|
||||||
Object.keys(before).forEach(function(beforeKey) {
|
Object.keys(before).forEach(function(beforeKey) {
|
||||||
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
|
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
|
||||||
delta[beforeKey]--; // keys present in the past have -ve values
|
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 });
|
results.push({ place: "del", key: muxedKey, val: beforeVal });
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 0: // A mix of added/removed keys
|
case 0: {// A mix of added/removed keys
|
||||||
// compare old & new vals
|
// compare old & new vals
|
||||||
var itemDelta = {};
|
const itemDelta = {};
|
||||||
before[muxedKey].forEach(function(beforeVal) {
|
before[muxedKey].forEach(function(beforeVal) {
|
||||||
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
|
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
|
||||||
itemDelta[beforeVal]--;
|
itemDelta[beforeVal]--;
|
||||||
|
@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
console.error("Calculated key delta of " + delta[muxedKey] +
|
console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
|
||||||
" - this should never happen!");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shallow-compare two objects for equality: each key and value must be
|
* Shallow-compare two objects for equality: each key and value must be identical
|
||||||
* 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) {
|
module.exports.shallowEqual = function(objA, objB) {
|
||||||
if (objA === objB) {
|
if (objA === objB) {
|
||||||
|
@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var keysA = Object.keys(objA);
|
const keysA = Object.keys(objA);
|
||||||
var keysB = Object.keys(objB);
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
if (keysA.length !== keysB.length) {
|
if (keysA.length !== keysB.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < keysA.length; i++) {
|
for (let i = 0; i < keysA.length; i++) {
|
||||||
var key = keysA[i];
|
const key = keysA[i];
|
||||||
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
|
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,4 +23,6 @@ export default {
|
||||||
CreateRoom: "create_room",
|
CreateRoom: "create_room",
|
||||||
RoomDirectory: "room_directory",
|
RoomDirectory: "room_directory",
|
||||||
UserView: "user_view",
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,7 +34,7 @@ class PasswordReset {
|
||||||
constructor(homeserverUrl, identityUrl) {
|
constructor(homeserverUrl, identityUrl) {
|
||||||
this.client = Matrix.createClient({
|
this.client = Matrix.createClient({
|
||||||
baseUrl: homeserverUrl,
|
baseUrl: homeserverUrl,
|
||||||
idBaseUrl: identityUrl
|
idBaseUrl: identityUrl,
|
||||||
});
|
});
|
||||||
this.clientSecret = this.client.generateClientSecret();
|
this.clientSecret = this.client.generateClientSecret();
|
||||||
this.identityServerDomain = identityUrl.split("://")[1];
|
this.identityServerDomain = identityUrl.split("://")[1];
|
||||||
|
@ -53,7 +53,7 @@ class PasswordReset {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, 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');
|
err.message = _t('This email address was not found');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
|
@ -75,16 +75,15 @@ class PasswordReset {
|
||||||
threepid_creds: {
|
threepid_creds: {
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: this.identityServerDomain
|
id_server: this.identityServerDomain,
|
||||||
}
|
},
|
||||||
}, this.password).catch(function(err) {
|
}, this.password).catch(function(err) {
|
||||||
if (err.httpStatus === 401) {
|
if (err.httpStatus === 401) {
|
||||||
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||||
}
|
} else if (err.httpStatus === 404) {
|
||||||
else if (err.httpStatus === 404) {
|
err.message =
|
||||||
err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
|
_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) {
|
|
||||||
err.message += ` (Status ${err.httpStatus})`;
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var dis = require('./dispatcher');
|
import dis from './dispatcher';
|
||||||
var sdk = require('./index');
|
|
||||||
var Modal = require('./Modal');
|
|
||||||
import { EventStatus } from 'matrix-js-sdk';
|
import { EventStatus } from 'matrix-js-sdk';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -37,12 +35,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
resend: function(event) {
|
resend: function(event) {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
MatrixClientPeg.get().resendEvent(
|
MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
|
||||||
event, room
|
|
||||||
).done(function(res) {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
|
@ -58,7 +54,7 @@ module.exports = {
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -66,7 +62,7 @@ module.exports = {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_cancelled',
|
action: 'message_send_cancelled',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
109
src/RichText.js
109
src/RichText.js
|
@ -16,6 +16,7 @@ import * as sdk from './index';
|
||||||
import * as emojione from 'emojione';
|
import * as emojione from 'emojione';
|
||||||
import {stateToHTML} from 'draft-js-export-html';
|
import {stateToHTML} from 'draft-js-export-html';
|
||||||
import {SelectionRange} from "./autocomplete/Autocompleter";
|
import {SelectionRange} from "./autocomplete/Autocompleter";
|
||||||
|
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||||
|
|
||||||
const MARKDOWN_REGEX = {
|
const MARKDOWN_REGEX = {
|
||||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||||
|
@ -30,10 +31,28 @@ const USERNAME_REGEX = /@\S+:\S+/g;
|
||||||
const ROOM_REGEX = /#\S+:\S+/g;
|
const ROOM_REGEX = /#\S+:\S+/g;
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, '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 ContentState.createFromBlockArray(convertFromHTML(html));
|
return stateToHTML(contentState, {
|
||||||
|
inlineStyles: {
|
||||||
|
UNDERLINE: {
|
||||||
|
element: 'u'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function htmlToContentState(html: string): ContentState {
|
||||||
|
const blockArray = convertFromHTML(html).contentBlocks;
|
||||||
|
return ContentState.createFromBlockArray(blockArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unicodeToEmojiUri(str) {
|
function unicodeToEmojiUri(str) {
|
||||||
|
@ -72,7 +91,7 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb
|
||||||
|
|
||||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||||
let emojiDecorator = {
|
let emojiDecorator = {
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => {
|
component: (props) => {
|
||||||
|
@ -95,38 +114,13 @@ let emojiDecorator = {
|
||||||
* Returns a composite decorator which has access to provided scope.
|
* Returns a composite decorator which has access to provided scope.
|
||||||
*/
|
*/
|
||||||
export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
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];
|
return [emojiDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||||
(style) => ({
|
(style) => ({
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
|
@ -137,7 +131,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
markdownDecorators.push({
|
markdownDecorators.push({
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
|
@ -146,9 +140,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
markdownDecorators.push(emojiDecorator);
|
// markdownDecorators.push(emojiDecorator);
|
||||||
|
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||||
return markdownDecorators;
|
return [emojiDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,31 +202,36 @@ export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||||
export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
||||||
contentBlocks: Array<ContentBlock>): SelectionState {
|
contentBlocks: Array<ContentBlock>): SelectionState {
|
||||||
let selectionState = SelectionState.createEmpty();
|
let selectionState = SelectionState.createEmpty();
|
||||||
|
// Subtract block lengths from `start` and `end` until they are less than the current
|
||||||
for (let block of contentBlocks) {
|
// block length (accounting for the NL at the end of each block). Set them to -1 to
|
||||||
let blockLength = block.getLength();
|
// indicate that the corresponding selection state has been determined.
|
||||||
|
for (const block of contentBlocks) {
|
||||||
if (start !== -1 && start < blockLength) {
|
const blockLength = block.getLength();
|
||||||
|
// -1 indicating that the position start position has been found
|
||||||
|
if (start !== -1) {
|
||||||
|
if (start < blockLength + 1) {
|
||||||
selectionState = selectionState.merge({
|
selectionState = selectionState.merge({
|
||||||
anchorKey: block.getKey(),
|
anchorKey: block.getKey(),
|
||||||
anchorOffset: start,
|
anchorOffset: start,
|
||||||
});
|
});
|
||||||
start = -1;
|
start = -1; // selection state for the start calculated
|
||||||
} else {
|
} else {
|
||||||
start -= blockLength;
|
start -= blockLength + 1; // +1 to account for newline between blocks
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (end !== -1 && end <= blockLength) {
|
// -1 indicating that the position end position has been found
|
||||||
|
if (end !== -1) {
|
||||||
|
if (end < blockLength + 1) {
|
||||||
selectionState = selectionState.merge({
|
selectionState = selectionState.merge({
|
||||||
focusKey: block.getKey(),
|
focusKey: block.getKey(),
|
||||||
focusOffset: end,
|
focusOffset: end,
|
||||||
});
|
});
|
||||||
end = -1;
|
end = -1; // selection state for the end calculated
|
||||||
} else {
|
} else {
|
||||||
end -= blockLength;
|
end -= blockLength + 1; // +1 to account for newline between blocks
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectionState;
|
return selectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +248,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
const existingEntityKey = block.getEntityAt(start);
|
const existingEntityKey = block.getEntityAt(start);
|
||||||
if (existingEntityKey) {
|
if (existingEntityKey) {
|
||||||
// avoid manipulation in case the emoji already has an entity
|
// avoid manipulation in case the emoji already has an entity
|
||||||
const entity = Entity.get(existingEntityKey);
|
const entity = newContentState.getEntity(existingEntityKey);
|
||||||
if (entity && entity.get('type') === 'emoji') {
|
if (entity && entity.get('type') === 'emoji') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -259,7 +258,10 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
.set('anchorOffset', start)
|
.set('anchorOffset', start)
|
||||||
.set('focusOffset', end);
|
.set('focusOffset', end);
|
||||||
const emojiText = plainText.substring(start, end);
|
const emojiText = plainText.substring(start, end);
|
||||||
const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText });
|
newContentState = newContentState.createEntity(
|
||||||
|
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText }
|
||||||
|
);
|
||||||
|
const entityKey = newContentState.getLastCreatedEntityKey();
|
||||||
newContentState = Modifier.replaceText(
|
newContentState = Modifier.replaceText(
|
||||||
newContentState,
|
newContentState,
|
||||||
selection,
|
selection,
|
||||||
|
@ -286,3 +288,14 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
|
|
||||||
return editorState;
|
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,8 +19,7 @@ limitations under the License.
|
||||||
function tsOfNewestEvent(room) {
|
function tsOfNewestEvent(room) {
|
||||||
if (room.timeline.length) {
|
if (room.timeline.length) {
|
||||||
return room.timeline[room.timeline.length - 1].getTs();
|
return room.timeline[room.timeline.length - 1].getTs();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Number.MAX_SAFE_INTEGER;
|
return Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mostRecentActivityFirst: mostRecentActivityFirst
|
mostRecentActivityFirst,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
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_LOUD = 'all_messages_loud';
|
||||||
export const ALL_MESSAGES = 'all_messages';
|
export const ALL_MESSAGES = 'all_messages';
|
||||||
|
@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRoomNotifsState(roomId, newState) {
|
export function setRoomNotifsState(roomId, newState) {
|
||||||
if (newState == MUTE) {
|
if (newState === MUTE) {
|
||||||
return setRoomNotifsStateMuted(roomId);
|
return setRoomNotifsStateMuted(roomId);
|
||||||
} else {
|
} else {
|
||||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||||
|
@ -80,14 +80,14 @@ function setRoomNotifsStateMuted(roomId) {
|
||||||
kind: 'event_match',
|
kind: 'event_match',
|
||||||
key: 'room_id',
|
key: 'room_id',
|
||||||
pattern: roomId,
|
pattern: roomId,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
'dont_notify',
|
'dont_notify',
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return q.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
|
@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState == 'all_messages') {
|
if (newState === 'all_messages') {
|
||||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||||
if (roomRule) {
|
if (roomRule) {
|
||||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
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, {
|
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||||
actions: [
|
actions: [
|
||||||
'dont_notify',
|
'dont_notify',
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// https://matrix.org/jira/browse/SPEC-400
|
||||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||||
|
@ -119,14 +119,14 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
{
|
{
|
||||||
set_tweak: 'sound',
|
set_tweak: 'sound',
|
||||||
value: 'default',
|
value: 'default',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// https://matrix.org/jira/browse/SPEC-400
|
||||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId) {
|
function findOverrideMuteRule(roomId) {
|
||||||
|
@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const cond = rule.conditions[0];
|
const cond = rule.conditions[0];
|
||||||
if (
|
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
||||||
cond.kind == 'event_match' &&
|
|
||||||
cond.key == 'room_id' &&
|
|
||||||
cond.pattern == roomId
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMuteRule(rule) {
|
function isMuteRule(rule) {
|
||||||
return (
|
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||||
rule.actions.length == 1 &&
|
|
||||||
rule.actions[0] == 'dont_notify'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a room object, return the alias we should use for it,
|
* 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) {
|
export function setDMRoom(roomId, userId) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return q();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var request = require('browser-request');
|
var request = require('browser-request');
|
||||||
|
|
||||||
var SdkConfig = require('./SdkConfig');
|
var SdkConfig = require('./SdkConfig');
|
||||||
|
@ -39,7 +39,7 @@ class ScalarAuthClient {
|
||||||
// Returns a scalar_token string
|
// Returns a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken() {
|
||||||
var tok = window.localStorage.getItem("mx_scalar_token");
|
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
|
// No saved token, so do the dance to get one. First, we
|
||||||
// need an openid bearer token from the HS.
|
// need an openid bearer token from the HS.
|
||||||
|
@ -53,7 +53,7 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeForScalarToken(openid_token_object) {
|
exchangeForScalarToken(openid_token_object) {
|
||||||
var defer = q.defer();
|
var defer = Promise.defer();
|
||||||
|
|
||||||
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||||
request({
|
request({
|
||||||
|
@ -76,10 +76,13 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId) {
|
getScalarInterfaceUrlForRoom(roomId, screen) {
|
||||||
var url = SdkConfig.get().integrations_ui_url;
|
var url = SdkConfig.get().integrations_ui_url;
|
||||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
url += "&room_id=" + encodeURIComponent(roomId);
|
url += "&room_id=" + encodeURIComponent(roomId);
|
||||||
|
if (screen) {
|
||||||
|
url += '&screen=' + encodeURIComponent(screen);
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,4 +92,3 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ScalarAuthClient;
|
module.exports = ScalarAuthClient;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,7 +18,7 @@ limitations under the License.
|
||||||
/*
|
/*
|
||||||
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
|
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,
|
room_id: $ROOM_ID,
|
||||||
user_id: $USER_ID
|
user_id: $USER_ID
|
||||||
// additional request fields
|
// additional request fields
|
||||||
|
@ -109,6 +110,99 @@ Example:
|
||||||
response: 78
|
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
|
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) {
|
function setPlumbingState(event, roomId, status) {
|
||||||
if (typeof status !== 'string') {
|
if (typeof status !== 'string') {
|
||||||
throw new Error('Plumbing state status should be a string');
|
throw new Error('Plumbing state status should be a string');
|
||||||
|
@ -287,6 +462,42 @@ function getMembershipCount(event, roomId) {
|
||||||
sendResponse(event, count);
|
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) {
|
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -332,7 +543,7 @@ const onMessage = function(event) {
|
||||||
// All strings start with the empty string, so for sanity return if the length
|
// All strings start with the empty string, so for sanity return if the length
|
||||||
// of the event origin is 0.
|
// of the event origin is 0.
|
||||||
let url = SdkConfig.get().integrations_ui_url;
|
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
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting join rules does not require userId
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
@ -377,6 +588,15 @@ const onMessage = function(event) {
|
||||||
} else if (event.data.action === "get_membership_count") {
|
} else if (event.data.action === "get_membership_count") {
|
||||||
getMembershipCount(event, roomId);
|
getMembershipCount(event, roomId);
|
||||||
return;
|
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) {
|
if (!userId) {
|
||||||
|
@ -409,12 +629,27 @@ const onMessage = function(event) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let listenerCount = 0;
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startListening: function() {
|
startListening: function() {
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.addEventListener("message", onMessage, false);
|
window.addEventListener("message", onMessage, false);
|
||||||
|
}
|
||||||
|
listenerCount += 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
stopListening: function() {
|
stopListening: function() {
|
||||||
|
listenerCount -= 1;
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.removeEventListener("message", onMessage);
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var DEFAULTS = {
|
const DEFAULTS = {
|
||||||
// URL to a page we show in an iframe to configure integrations
|
// URL to a page we show in an iframe to configure integrations
|
||||||
integrations_ui_url: "https://scalar.vector.im/",
|
integrations_ui_url: "https://scalar.vector.im/",
|
||||||
// Base URL to the REST interface of the integrations server
|
// Base URL to the REST interface of the integrations server
|
||||||
|
@ -30,8 +30,8 @@ class SdkConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
static put(cfg) {
|
static put(cfg) {
|
||||||
var defaultKeys = Object.keys(DEFAULTS);
|
const defaultKeys = Object.keys(DEFAULTS);
|
||||||
for (var i = 0; i < defaultKeys.length; ++i) {
|
for (let i = 0; i < defaultKeys.length; ++i) {
|
||||||
if (cfg[defaultKeys[i]] === undefined) {
|
if (cfg[defaultKeys[i]] === undefined) {
|
||||||
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
|
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,19 +51,18 @@ class Skinner {
|
||||||
if (this.components !== null) {
|
if (this.components !== null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load a skin while a skin is already loaded"+
|
"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 = {};
|
this.components = {};
|
||||||
var compKeys = Object.keys(skinObject.components);
|
const compKeys = Object.keys(skinObject.components);
|
||||||
for (var i = 0; i < compKeys.length; ++i) {
|
for (let i = 0; i < compKeys.length; ++i) {
|
||||||
var comp = skinObject.components[compKeys[i]];
|
const comp = skinObject.components[compKeys[i]];
|
||||||
this.addComponent(compKeys[i], comp);
|
this.addComponent(compKeys[i], comp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponent(name, comp) {
|
addComponent(name, comp) {
|
||||||
var slot = name;
|
let slot = name;
|
||||||
if (comp.replaces !== undefined) {
|
if (comp.replaces !== undefined) {
|
||||||
if (comp.replaces.indexOf('.') > -1) {
|
if (comp.replaces.indexOf('.') > -1) {
|
||||||
slot = comp.replaces;
|
slot = comp.replaces;
|
||||||
|
|
|
@ -68,7 +68,7 @@ const commands = {
|
||||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||||
title: _t('/ddg is not a command'),
|
title: _t('/ddg is not a command'),
|
||||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||||
});
|
});
|
||||||
|
@ -301,27 +301,36 @@ const commands = {
|
||||||
const deviceId = matches[2];
|
const deviceId = matches[2];
|
||||||
const fingerprint = matches[3];
|
const fingerprint = matches[3];
|
||||||
|
|
||||||
const device = MatrixClientPeg.get().getStoredDevice(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) {
|
if (!device) {
|
||||||
return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.isVerified()) {
|
if (device.isVerified()) {
|
||||||
if (device.getFingerprint() === fingerprint) {
|
if (device.getFingerprint() === fingerprint) {
|
||||||
return reject(_t(`Device already verified!`));
|
throw new Error(_t(`Device already verified!`));
|
||||||
} else {
|
} else {
|
||||||
return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.getFingerprint() === fingerprint) {
|
if (device.getFingerprint() !== fingerprint) {
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
const fprint = device.getFingerprint();
|
||||||
userId, deviceId, true,
|
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!
|
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
||||||
|
}).then(() => {
|
||||||
|
// Tell the user we verified everything
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||||
title: _t("Verified key"),
|
title: _t("Verified key"),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
|
@ -336,19 +345,10 @@ const commands = {
|
||||||
),
|
),
|
||||||
hasCancelButton: false,
|
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 reject(this.getUsage());
|
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;
|
|
|
@ -24,7 +24,7 @@ const onAction = function(payload) {
|
||||||
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||||
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
||||||
devices: payload.err.devices,
|
devices: payload.err.devices,
|
||||||
room: payload.room,
|
room: payload.room,
|
||||||
onFinished: (r) => {
|
onFinished: (r) => {
|
||||||
|
|
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
import UserSettingsStore from './UserSettingsStore';
|
||||||
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -37,13 +39,33 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
doesRoomHaveUnreadMessages: function(room) {
|
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
|
// 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
|
// 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
|
// don't count, we don't know if there are any events that do count between where
|
||||||
// we have and the read receipt. We could fetch more history to try & find out,
|
// we have and the read receipt. We could fetch more history to try & find out,
|
||||||
// but currently we just guess.
|
// but currently we just guess.
|
||||||
|
|
||||||
|
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
// Loop through messages, starting with the most recent...
|
// Loop through messages, starting with the most recent...
|
||||||
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
var ev = room.timeline[i];
|
var ev = room.timeline[i];
|
||||||
|
@ -53,7 +75,7 @@ module.exports = {
|
||||||
// that counts and we can stop looking because the user's read
|
// that counts and we can stop looking because the user's read
|
||||||
// this and everything before.
|
// this and everything before.
|
||||||
return false;
|
return false;
|
||||||
} else if (this.eventTriggersUnreadCount(ev)) {
|
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) {
|
||||||
// We've found a message that counts before we hit
|
// We've found a message that counts before we hit
|
||||||
// the read marker, so this room is definitely unread.
|
// the read marker, so this room is definitely unread.
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var dis = require("./dispatcher");
|
import dis from './dispatcher';
|
||||||
|
|
||||||
var MIN_DISPATCH_INTERVAL_MS = 500;
|
const MIN_DISPATCH_INTERVAL_MS = 500;
|
||||||
var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class watches for user activity (moving the mouse or pressing a key)
|
* 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
|
* Return true if there has been user activity very recently
|
||||||
* (ie. within a few seconds)
|
* (ie. within a few seconds)
|
||||||
|
* @returns {boolean} true if user is currently/very recently active
|
||||||
*/
|
*/
|
||||||
userCurrentlyActive() {
|
userCurrentlyActive() {
|
||||||
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
|
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserActivity(event) {
|
_onUserActivity(event) {
|
||||||
if (event.screenX && event.type == "mousemove") {
|
if (event.screenX && event.type === "mousemove") {
|
||||||
if (event.screenX === this.lastScreenX &&
|
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
||||||
event.screenY === this.lastScreenY)
|
|
||||||
{
|
|
||||||
// mouse hasn't actually moved
|
// mouse hasn't actually moved
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -79,28 +78,24 @@ class UserActivity {
|
||||||
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
||||||
this.lastDispatchAtTs = this.lastActivityAtTs;
|
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'user_activity'
|
action: 'user_activity',
|
||||||
});
|
});
|
||||||
if (!this.activityEndTimer) {
|
if (!this.activityEndTimer) {
|
||||||
this.activityEndTimer = setTimeout(
|
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS);
|
||||||
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onActivityEndTimer() {
|
_onActivityEndTimer() {
|
||||||
var now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
||||||
if (now >= targetTime) {
|
if (now >= targetTime) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'user_activity_end'
|
action: 'user_activity_end',
|
||||||
});
|
});
|
||||||
this.activityEndTimer = undefined;
|
this.activityEndTimer = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.activityEndTimer = setTimeout(
|
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now);
|
||||||
this._onActivityEndTimer.bind(this), targetTime - now
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import Notifier from './Notifier';
|
import Notifier from './Notifier';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
@ -27,14 +27,17 @@ export default {
|
||||||
LABS_FEATURES: [
|
LABS_FEATURES: [
|
||||||
{
|
{
|
||||||
name: "-",
|
name: "-",
|
||||||
id: 'rich_text_editor',
|
id: 'matrix_apps',
|
||||||
default: false,
|
default: false,
|
||||||
|
|
||||||
|
// XXX: Always use default, ignore localStorage and remove from labs
|
||||||
|
override: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// horrible but it works. The locality makes this somewhat more palatable.
|
// horrible but it works. The locality makes this somewhat more palatable.
|
||||||
doTranslations: function() {
|
doTranslations: function() {
|
||||||
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
|
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
@ -48,7 +51,7 @@ export default {
|
||||||
|
|
||||||
loadThreePids: function() {
|
loadThreePids: function() {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
threepids: [],
|
threepids: [],
|
||||||
}); // guests can't poke 3pid endpoint
|
}); // guests can't poke 3pid endpoint
|
||||||
}
|
}
|
||||||
|
@ -171,22 +174,36 @@ export default {
|
||||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||||
},
|
},
|
||||||
|
|
||||||
isFeatureEnabled: function(feature: string): boolean {
|
getFeatureById(feature: string) {
|
||||||
// Disable labs for guests.
|
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
|
||||||
|
|
||||||
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
|
||||||
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||||
const f = this.LABS_FEATURES[i];
|
const f = this.LABS_FEATURES[i];
|
||||||
if (f.id === feature) {
|
if (f.id === feature) {
|
||||||
return f.default;
|
return f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
isFeatureEnabled: function(featureId: string): boolean {
|
||||||
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
// Disable labs for guests.
|
||||||
|
if (MatrixClientPeg.get().isGuest()) return false;
|
||||||
|
|
||||||
|
const feature = this.getFeatureById(featureId);
|
||||||
|
if (!feature) {
|
||||||
|
console.warn(`Unknown feature "${featureId}"`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return the default if this feature has an override to be the default value or
|
||||||
|
// if the feature has never been toggled and is therefore not in localStorage
|
||||||
|
if (Object.keys(feature).includes('override') ||
|
||||||
|
localStorage.getItem(`mx_labs_feature_${featureId}`) === null
|
||||||
|
) {
|
||||||
|
return feature.default;
|
||||||
|
}
|
||||||
|
return localStorage.getItem(`mx_labs_feature_${featureId}`) === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
|
setFeatureEnabled: function(featureId: string, enabled: boolean) {
|
||||||
|
localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,7 +64,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
//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;
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
}
|
}
|
||||||
self.children[c.key] = old;
|
self.children[c.key] = old;
|
||||||
|
|
58
src/WidgetUtils.js
Normal file
58
src/WidgetUtils.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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 MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
|
export default class WidgetUtils {
|
||||||
|
|
||||||
|
/* Returns true if user is able to send state events to modify widgets in this room
|
||||||
|
* @param roomId -- The ID of the room to check
|
||||||
|
* @return Boolean -- true if the user can modify widgets in this room
|
||||||
|
* @throws Error -- specifies the error reason
|
||||||
|
*/
|
||||||
|
static canUserModifyWidgets(roomId) {
|
||||||
|
if (!roomId) {
|
||||||
|
console.warn('No room ID specified');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
console.warn('User must be be logged in');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
console.warn(`Room ID ${roomId} is not recognised`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = client.credentials.userId;
|
||||||
|
if (!me) {
|
||||||
|
console.warn('Failed to get user ID');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = room.getMember(me);
|
||||||
|
if (!member || member.membership !== "join") {
|
||||||
|
console.warn(`User ${me} is not in room ${roomId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,23 +28,31 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return { device: this.refreshDevice() };
|
return { device: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
|
||||||
|
|
||||||
// no need to redownload keys if we already have the device
|
// first try to load the device from our store.
|
||||||
if (this.state.device) {
|
//
|
||||||
return;
|
this.refreshDevice().then((dev) => {
|
||||||
|
if (dev) {
|
||||||
|
return dev;
|
||||||
}
|
}
|
||||||
client.downloadKeys([this.props.event.getSender()], true).done(()=>{
|
|
||||||
|
// 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) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ device: this.refreshDevice() });
|
|
||||||
|
this.setState({ device: dev });
|
||||||
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
}, (err)=>{
|
}, (err)=>{
|
||||||
console.log("Error downloading devices", err);
|
console.log("Error downloading devices", err);
|
||||||
});
|
});
|
||||||
|
@ -59,12 +67,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshDevice: function() {
|
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) {
|
onDeviceVerificationChanged: function(userId, device) {
|
||||||
if (userId == this.props.event.getSender()) {
|
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';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp) {
|
||||||
if (commandRegex) {
|
if (commandRegex) {
|
||||||
if (!commandRegex.global) {
|
if (!commandRegex.global) {
|
||||||
throw new Error('commandRegex must have global flag set');
|
throw new Error('commandRegex must have global flag set');
|
||||||
|
|
|
@ -22,7 +22,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
import Q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
export type SelectionRange = {
|
export type SelectionRange = {
|
||||||
start: number,
|
start: number,
|
||||||
|
@ -34,6 +34,9 @@ export type Completion = {
|
||||||
component: ?Component,
|
component: ?Component,
|
||||||
range: SelectionRange,
|
range: SelectionRange,
|
||||||
command: ?string,
|
command: ?string,
|
||||||
|
// If provided, apply a LINK entity to the completion with the
|
||||||
|
// data = { url: href }.
|
||||||
|
href: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
@ -52,21 +55,24 @@ export async function getCompletions(query: string, selection: SelectionRange, f
|
||||||
otherwise, we run into a condition where new completions are displayed
|
otherwise, we run into a condition where new completions are displayed
|
||||||
while the user is interacting with the list, which makes it difficult
|
while the user is interacting with the list, which makes it difficult
|
||||||
to predict whether an action will actually do what is intended
|
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
|
const completionsList = await Promise.all(
|
||||||
state (== "fulfilled" || "rejected") and value. */
|
// Array of inspections of promises that might timeout. Instead of allowing a
|
||||||
const completionsList = await Q.allSettled(
|
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||||
PROVIDERS.map(provider => {
|
// settled, filter for the fulfilled ones
|
||||||
return Q(provider.getCompletions(query, selection, force))
|
PROVIDERS.map((provider) => {
|
||||||
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
return provider
|
||||||
})
|
.getCompletions(query, selection, force)
|
||||||
|
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||||
|
.reflect();
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return completionsList
|
return completionsList.filter(
|
||||||
.filter(completion => completion.state === "fulfilled")
|
(inspection) => inspection.isFulfilled(),
|
||||||
.map((completionsState, i) => {
|
).map((completionsState, i) => {
|
||||||
return {
|
return {
|
||||||
completions: completionsState.value,
|
completions: completionsState.value(),
|
||||||
provider: PROVIDERS[i],
|
provider: PROVIDERS[i],
|
||||||
|
|
||||||
/* the currently matched "command" the completer tried to complete
|
/* the currently matched "command" the completer tried to complete
|
||||||
|
|
|
@ -18,9 +18,10 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
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
|
// 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 = [
|
const COMMANDS = [
|
||||||
{
|
{
|
||||||
|
@ -33,6 +34,16 @@ const COMMANDS = [
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Bans user with given id',
|
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',
|
command: '/deop',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
|
@ -48,6 +59,16 @@ const COMMANDS = [
|
||||||
args: '<room-alias>',
|
args: '<room-alias>',
|
||||||
description: 'Joins room with given 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',
|
command: '/kick',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
|
@ -63,6 +84,17 @@ const COMMANDS = [
|
||||||
args: '<query>',
|
args: '<query>',
|
||||||
description: 'Searches DuckDuckGo for results',
|
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;
|
const COMMAND_RE = /(^\/\w*)/g;
|
||||||
|
@ -72,7 +104,7 @@ let instance = null;
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMAND_RE);
|
super(COMMAND_RE);
|
||||||
this.fuse = new Fuse(COMMANDS, {
|
this.matcher = new FuzzyMatcher(COMMANDS, {
|
||||||
keys: ['command', 'args', 'description'],
|
keys: ['command', 'args', 'description'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -81,7 +113,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map((result) => {
|
completions = this.matcher.match(command[0]).map((result) => {
|
||||||
return {
|
return {
|
||||||
completion: result.command + ' ',
|
completion: result.command + ' ',
|
||||||
component: (<TextualCompletion
|
component: (<TextualCompletion
|
||||||
|
|
|
@ -18,31 +18,117 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
|
import _uniq from 'lodash/uniq';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
import EmojiData from '../stripped-emoji.json';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
|
||||||
|
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;
|
let instance = null;
|
||||||
|
|
||||||
|
function score(query, space) {
|
||||||
|
const index = space.indexOf(query);
|
||||||
|
if (index === -1) {
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
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) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
let matchedString = command[0];
|
||||||
const shortname = EMOJI_SHORTNAMES[result];
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
const sorters = [];
|
||||||
|
// First, sort by score (Infinity if matchedString not in shortname)
|
||||||
|
sorters.push((c) => score(matchedString, c.shortname));
|
||||||
|
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||||
|
// matchedString = ":bookmark"
|
||||||
|
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
||||||
|
if (matchedString.length > 1) {
|
||||||
|
sorters.push((c) => c.shortname.length);
|
||||||
|
}
|
||||||
|
// Finally, sort by original ordering
|
||||||
|
sorters.push((c) => c._orderBy);
|
||||||
|
completions = _sortBy(_uniq(completions), sorters);
|
||||||
|
|
||||||
|
completions = completions.map((result) => {
|
||||||
|
const {shortname} = result;
|
||||||
const unicode = shortnameToUnicode(shortname);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
|
@ -51,7 +137,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 8);
|
}).slice(0, LIMIT);
|
||||||
}
|
}
|
||||||
return completions;
|
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,50 +19,75 @@ import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
|
function score(query, space) {
|
||||||
|
const index = space.indexOf(query);
|
||||||
|
if (index === -1) {
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class RoomProvider extends AutocompleteProvider {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX, {
|
super(ROOM_REGEX);
|
||||||
keys: ['displayName', 'userId'],
|
this.matcher = new FuzzyMatcher([], {
|
||||||
});
|
keys: ['displayedAlias', 'name'],
|
||||||
this.fuse = new Fuse([], {
|
|
||||||
keys: ['name', 'roomId', 'aliases'],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||||
|
if (/^(\/join|\/leave)/.test(query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// 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 {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
aliases: room.getAliases(),
|
displayedAlias: getDisplayAliasForRoom(room),
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
completions = this.fuse.search(command[0]).map(room => {
|
const matchedString = command[0];
|
||||||
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
completions = this.matcher.match(matchedString);
|
||||||
|
completions = _sortBy(completions, [
|
||||||
|
(c) => score(matchedString, c.displayedAlias),
|
||||||
|
(c) => c.displayedAlias.length,
|
||||||
|
]).map((room) => {
|
||||||
|
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
|
suffix: ' ',
|
||||||
|
href: 'https://matrix.to/#/' + displayAlias,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).filter(completion => !!completion.completion && completion.completion.length > 0).slice(0, 4);
|
})
|
||||||
|
.filter((completion) => !!completion.completion && completion.completion.length > 0)
|
||||||
|
.slice(0, 4);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
@ -80,12 +105,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
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}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldForceComplete(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//@flow
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
@ -18,42 +19,52 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Fuse from 'fuse.js';
|
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
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;
|
const USER_REGEX = /@\S*/g;
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
|
users: Array<RoomMember> = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name'],
|
||||||
});
|
});
|
||||||
this.users = [];
|
this.matcher = new FuzzyMatcher([], {
|
||||||
this.fuse = new Fuse([], {
|
keys: ['name'],
|
||||||
keys: ['name', 'userId'],
|
shouldMatchPrefix: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||||
|
if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
this.fuse.set(this.users);
|
completions = this.matcher.match(command[0]).map((user) => {
|
||||||
completions = this.fuse.search(command[0]).map(user => {
|
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
|
||||||
let completion = displayName;
|
|
||||||
if (range.start === 0) {
|
|
||||||
completion += ': ';
|
|
||||||
} else {
|
|
||||||
completion += ' ';
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
completion,
|
// Length of completion should equal length of text in decorator. draft-js
|
||||||
|
// relies on the length of the entity === length of the text in the decoration.
|
||||||
|
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
||||||
|
suffix: range.start === 0 ? ': ' : ' ',
|
||||||
|
href: 'https://matrix.to/#/' + user.userId,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion
|
<PillCompletion
|
||||||
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||||
|
@ -62,7 +73,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
});
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
@ -71,8 +82,35 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return '👥 ' + _t('Users');
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserList(users) {
|
setUserListFromRoom(room: Room) {
|
||||||
this.users = users;
|
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 {
|
static getInstance(): UserProvider {
|
||||||
|
@ -83,7 +121,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
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}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
531
src/components/structures/GroupView.js
Normal file
531
src/components/structures/GroupView.js
Normal file
|
@ -0,0 +1,531 @@
|
||||||
|
/*
|
||||||
|
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';
|
||||||
|
import Modal from '../../Modal';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
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,
|
||||||
|
saving: false,
|
||||||
|
uploadingAvatar: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._changeAvatarComponent = null;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEditClick: function() {
|
||||||
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
profileForm: Object.assign({}, this.state.summary.profile),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function() {
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
profileForm: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onNameChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onShortDescChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onLongDescChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAvatarSelected: function(ev) {
|
||||||
|
const file = ev.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
this.setState({uploadingAvatar: true});
|
||||||
|
MatrixClientPeg.get().uploadContent(file).then((url) => {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
|
||||||
|
this.setState({
|
||||||
|
uploadingAvatar: false,
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({uploadingAvatar: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to upload avatar image", e);
|
||||||
|
Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
|
||||||
|
title: _t('Error'),
|
||||||
|
description: _t('Failed to upload image'),
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSaveClick: function() {
|
||||||
|
this.setState({saving: true});
|
||||||
|
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm).then((result) => {
|
||||||
|
this.setState({
|
||||||
|
saving: false,
|
||||||
|
editing: false,
|
||||||
|
summary: null,
|
||||||
|
});
|
||||||
|
this._loadGroupFromServer(this.props.groupId);
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to save group profile", e);
|
||||||
|
Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
|
||||||
|
title: _t('Error'),
|
||||||
|
description: _t('Failed to update group'),
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
_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 || this.state.saving) {
|
||||||
|
return <Loader />;
|
||||||
|
} else if (this.state.summary) {
|
||||||
|
const summary = this.state.summary;
|
||||||
|
|
||||||
|
let avatarNode;
|
||||||
|
let nameNode;
|
||||||
|
let shortDescNode;
|
||||||
|
let rightButtons;
|
||||||
|
let roomBody;
|
||||||
|
const headerClasses = {
|
||||||
|
mx_GroupView_header: true,
|
||||||
|
};
|
||||||
|
if (this.state.editing) {
|
||||||
|
let avatarImage;
|
||||||
|
if (this.state.uploadingAvatar) {
|
||||||
|
avatarImage = <Loader />;
|
||||||
|
} else {
|
||||||
|
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||||
|
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
||||||
|
groupAvatarUrl={this.state.profileForm.avatar_url}
|
||||||
|
width={48} height={48} resizeMethod='crop'
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarNode = (
|
||||||
|
<div className="mx_GroupView_avatarPicker">
|
||||||
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
|
{avatarImage}
|
||||||
|
</label>
|
||||||
|
<div className="mx_GroupView_avatarPicker_edit">
|
||||||
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
|
<img src="img/camera.svg"
|
||||||
|
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
||||||
|
width="17" height="15" />
|
||||||
|
</label>
|
||||||
|
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
nameNode = <input type="text"
|
||||||
|
value={this.state.profileForm.name}
|
||||||
|
onChange={this._onNameChange}
|
||||||
|
placeholder={_t('Group Name')}
|
||||||
|
tabIndex="1"
|
||||||
|
/>;
|
||||||
|
shortDescNode = <input type="text"
|
||||||
|
value={this.state.profileForm.short_description}
|
||||||
|
onChange={this._onShortDescChange}
|
||||||
|
placeholder={_t('Description')}
|
||||||
|
tabIndex="2"
|
||||||
|
/>;
|
||||||
|
rightButtons = <span>
|
||||||
|
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
||||||
|
{_t('Save')}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
||||||
|
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||||
|
width="18" height="18" alt={_t("Cancel")}/>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
roomBody = <div>
|
||||||
|
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
||||||
|
onChange={this._onLongDescChange}
|
||||||
|
tabIndex="3"
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||||
|
avatarNode = <GroupAvatar
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
groupAvatarUrl={groupAvatarUrl}
|
||||||
|
width={48} height={48}
|
||||||
|
/>;
|
||||||
|
if (summary.profile && summary.profile.name) {
|
||||||
|
nameNode = <div>
|
||||||
|
<span>{summary.profile.name}</span>
|
||||||
|
<span className="mx_GroupView_header_groupid">
|
||||||
|
({this.props.groupId})
|
||||||
|
</span>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
nameNode = <span>{this.props.groupId}</span>;
|
||||||
|
}
|
||||||
|
shortDescNode = <span>{summary.profile.short_description}</span>;
|
||||||
|
|
||||||
|
let description = null;
|
||||||
|
if (summary.profile && summary.profile.long_description) {
|
||||||
|
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||||
|
}
|
||||||
|
roomBody = <div>
|
||||||
|
<div className="mx_GroupView_groupDesc">{description}</div>
|
||||||
|
{this._getFeaturedRoomsNode()}
|
||||||
|
{this._getFeaturedUsersNode()}
|
||||||
|
</div>;
|
||||||
|
// disabled until editing works
|
||||||
|
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
onClick={this._onEditClick} title={_t("Edit Group")}
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||||
|
</AccessibleButton>;
|
||||||
|
|
||||||
|
headerClasses.mx_GroupView_header_view = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_GroupView">
|
||||||
|
<div className={classnames(headerClasses)}>
|
||||||
|
<div className="mx_GroupView_header_leftCol">
|
||||||
|
<div className="mx_GroupView_header_avatar">
|
||||||
|
{avatarNode}
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_header_info">
|
||||||
|
<div className="mx_GroupView_header_name">
|
||||||
|
{nameNode}
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_header_shortDesc">
|
||||||
|
{shortDescNode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_header_rightCol">
|
||||||
|
{rightButtons}
|
||||||
|
</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) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
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';
|
'view_prev_room' : 'view_next_room';
|
||||||
dis.dispatch({action: action});
|
dis.dispatch({action: action});
|
||||||
handled = true;
|
handled = true;
|
||||||
|
@ -184,6 +191,14 @@ export default React.createClass({
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case KeyCode.KEY_K:
|
||||||
|
if (ctrlCmdOnly) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'focus_room_filter',
|
||||||
|
});
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
|
@ -210,6 +225,8 @@ export default React.createClass({
|
||||||
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
const HomePage = sdk.getComponent('structures.HomePage');
|
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 MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||||
|
@ -247,6 +264,10 @@ export default React.createClass({
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PageTypes.MyGroups:
|
||||||
|
page_element = <MyGroups />;
|
||||||
|
break;
|
||||||
|
|
||||||
case PageTypes.CreateRoom:
|
case PageTypes.CreateRoom:
|
||||||
page_element = <CreateRoom
|
page_element = <CreateRoom
|
||||||
onRoomCreated={this.props.onRoomCreated}
|
onRoomCreated={this.props.onRoomCreated}
|
||||||
|
@ -263,6 +284,7 @@ export default React.createClass({
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.HomePage:
|
case PageTypes.HomePage:
|
||||||
|
{
|
||||||
// If team server config is present, pass the teamServerURL. props.teamToken
|
// If team server config is present, pass the teamServerURL. props.teamToken
|
||||||
// must also be set for the team page to be displayed, otherwise the
|
// must also be set for the team page to be displayed, otherwise the
|
||||||
// welcomePageUrl is used (which might be undefined).
|
// welcomePageUrl is used (which might be undefined).
|
||||||
|
@ -274,11 +296,18 @@ export default React.createClass({
|
||||||
teamToken={this.props.teamToken}
|
teamToken={this.props.teamToken}
|
||||||
homePageUrl={this.props.config.welcomePageUrl}
|
homePageUrl={this.props.config.welcomePageUrl}
|
||||||
/>;
|
/>;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserView:
|
case PageTypes.UserView:
|
||||||
page_element = null; // deliberately null for now
|
page_element = null; // deliberately null for now
|
||||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
|
break;
|
||||||
|
case PageTypes.GroupView:
|
||||||
|
page_element = <GroupView
|
||||||
|
groupId={this.props.currentGroupId}
|
||||||
|
/>;
|
||||||
|
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
|
@ -131,9 +131,6 @@ module.exports = React.createClass({
|
||||||
// the master view we are showing.
|
// the master view we are showing.
|
||||||
view: VIEWS.LOADING,
|
view: VIEWS.LOADING,
|
||||||
|
|
||||||
// a thing to call showScreen with once login completes.
|
|
||||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
|
||||||
|
|
||||||
// What the LoggedInView would be showing if visible
|
// What the LoggedInView would be showing if visible
|
||||||
page_type: null,
|
page_type: null,
|
||||||
|
|
||||||
|
@ -147,8 +144,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
ready: false,
|
|
||||||
width: 10000,
|
|
||||||
leftOpacity: 1.0,
|
leftOpacity: 1.0,
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
rightOpacity: 1.0,
|
rightOpacity: 1.0,
|
||||||
|
@ -224,7 +219,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Used by _viewRoom before getting state from sync
|
// Used by _viewRoom before getting state from sync
|
||||||
this.firstSyncComplete = false;
|
this.firstSyncComplete = false;
|
||||||
this.firstSyncPromise = q.defer();
|
this.firstSyncPromise = Promise.defer();
|
||||||
|
|
||||||
if (this.props.config.sync_timeline_limit) {
|
if (this.props.config.sync_timeline_limit) {
|
||||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||||
|
@ -274,6 +269,15 @@ module.exports = React.createClass({
|
||||||
register_hs_url: paramHs,
|
register_hs_url: paramHs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a thing to call showScreen with once login completes. this is kept
|
||||||
|
// outside this.state because updating it should never trigger a
|
||||||
|
// rerender.
|
||||||
|
this._screenAfterLogin = this.props.initialScreenAfterLogin;
|
||||||
|
|
||||||
|
this._windowWidth = 10000;
|
||||||
|
this.handleResize();
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -290,9 +294,9 @@ module.exports = React.createClass({
|
||||||
if (this.onUserClick) {
|
if (this.onUserClick) {
|
||||||
linkifyMatrix.onUserClick = this.onUserClick;
|
linkifyMatrix.onUserClick = this.onUserClick;
|
||||||
}
|
}
|
||||||
|
if (this.onGroupClick) {
|
||||||
window.addEventListener('resize', this.handleResize);
|
linkifyMatrix.onGroupClick = this.onGroupClick;
|
||||||
this.handleResize();
|
}
|
||||||
|
|
||||||
const teamServerConfig = this.props.config.teamServerConfig || {};
|
const teamServerConfig = this.props.config.teamServerConfig || {};
|
||||||
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
|
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
|
||||||
|
@ -309,20 +313,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// if the user has followed a login or register link, don't reanimate
|
// 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 old creds, but rather go straight to the relevant page
|
||||||
const firstScreen = this.state.screenAfterLogin ?
|
const firstScreen = this._screenAfterLogin ?
|
||||||
this.state.screenAfterLogin.screen : null;
|
this._screenAfterLogin.screen : null;
|
||||||
|
|
||||||
if (firstScreen === 'login' ||
|
if (firstScreen === 'login' ||
|
||||||
firstScreen === 'register' ||
|
firstScreen === 'register' ||
|
||||||
firstScreen === 'forgot_password') {
|
firstScreen === 'forgot_password') {
|
||||||
this.setState({loading: false});
|
|
||||||
this._showScreenAfterLogin();
|
this._showScreenAfterLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// the extra q() ensures that synchronous exceptions hit the same codepath as
|
// the extra Promise.resolve() ensures that synchronous exceptions hit the same codepath as
|
||||||
// asynchronous ones.
|
// asynchronous ones.
|
||||||
return q().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
return Lifecycle.loadSession({
|
return Lifecycle.loadSession({
|
||||||
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
||||||
enableGuest: this.props.enableGuest,
|
enableGuest: this.props.enableGuest,
|
||||||
|
@ -407,7 +410,7 @@ module.exports = React.createClass({
|
||||||
this._leaveRoom(payload.room_id);
|
this._leaveRoom(payload.room_id);
|
||||||
break;
|
break;
|
||||||
case 'reject_invite':
|
case 'reject_invite':
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
|
||||||
title: _t('Reject invitation'),
|
title: _t('Reject invitation'),
|
||||||
description: _t('Are you sure you want to reject the invitation?'),
|
description: _t('Are you sure you want to reject the invitation?'),
|
||||||
onFinished: (confirm) => {
|
onFinished: (confirm) => {
|
||||||
|
@ -423,7 +426,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to reject invitation', '', ErrorDialog, {
|
||||||
title: _t('Failed to reject invitation'),
|
title: _t('Failed to reject invitation'),
|
||||||
description: err.toString(),
|
description: err.toString(),
|
||||||
});
|
});
|
||||||
|
@ -483,6 +486,18 @@ module.exports = React.createClass({
|
||||||
this._setPage(PageTypes.RoomDirectory);
|
this._setPage(PageTypes.RoomDirectory);
|
||||||
this.notifyNewScreen('directory');
|
this.notifyNewScreen('directory');
|
||||||
break;
|
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':
|
case 'view_home_page':
|
||||||
this._setPage(PageTypes.HomePage);
|
this._setPage(PageTypes.HomePage);
|
||||||
this.notifyNewScreen('home');
|
this.notifyNewScreen('home');
|
||||||
|
@ -491,7 +506,7 @@ module.exports = React.createClass({
|
||||||
this._setMxId(payload);
|
this._setMxId(payload);
|
||||||
break;
|
break;
|
||||||
case 'view_start_chat_or_reuse':
|
case 'view_start_chat_or_reuse':
|
||||||
this._chatCreateOrReuse(payload.user_id);
|
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
|
||||||
break;
|
break;
|
||||||
case 'view_create_chat':
|
case 'view_create_chat':
|
||||||
this._createChat();
|
this._createChat();
|
||||||
|
@ -548,7 +563,12 @@ module.exports = React.createClass({
|
||||||
this._onLoggedOut();
|
this._onLoggedOut();
|
||||||
break;
|
break;
|
||||||
case 'will_start_client':
|
case 'will_start_client':
|
||||||
|
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();
|
this._onWillStartClient();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'new_version':
|
case 'new_version':
|
||||||
this.onVersion(
|
this.onVersion(
|
||||||
|
@ -674,7 +694,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Wait for the first sync to complete so that if a room does have an alias,
|
// Wait for the first sync to complete so that if a room does have an alias,
|
||||||
// it would have been retrieved.
|
// it would have been retrieved.
|
||||||
let waitFor = q(null);
|
let waitFor = Promise.resolve(null);
|
||||||
if (!this.firstSyncComplete) {
|
if (!this.firstSyncComplete) {
|
||||||
if (!this.firstSyncPromise) {
|
if (!this.firstSyncPromise) {
|
||||||
console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
|
console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
|
||||||
|
@ -708,7 +728,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_setMxId: function(payload) {
|
_setMxId: function(payload) {
|
||||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||||
const close = Modal.createDialog(SetMxIdDialog, {
|
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||||
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
onFinished: (submitted, credentials) => {
|
onFinished: (submitted, credentials) => {
|
||||||
if (!submitted) {
|
if (!submitted) {
|
||||||
|
@ -747,7 +767,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
Modal.createDialog(ChatInviteDialog, {
|
Modal.createTrackedDialog('Start a chat', '', ChatInviteDialog, {
|
||||||
title: _t('Start a chat'),
|
title: _t('Start a chat'),
|
||||||
description: _t("Who would you like to communicate with?"),
|
description: _t("Who would you like to communicate with?"),
|
||||||
placeholder: _t("Email, name or matrix ID"),
|
placeholder: _t("Email, name or matrix ID"),
|
||||||
|
@ -767,7 +787,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||||
Modal.createDialog(TextInputDialog, {
|
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
|
||||||
title: _t('Create Room'),
|
title: _t('Create Room'),
|
||||||
description: _t('Room name (optional)'),
|
description: _t('Room name (optional)'),
|
||||||
button: _t('Create Room'),
|
button: _t('Create Room'),
|
||||||
|
@ -781,7 +801,9 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_chatCreateOrReuse: function(userId) {
|
_chatCreateOrReuse: function(userId, goHomeOnCancel) {
|
||||||
|
if (goHomeOnCancel === undefined) goHomeOnCancel = true;
|
||||||
|
|
||||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||||
'views.dialogs.ChatCreateOrReuseDialog',
|
'views.dialogs.ChatCreateOrReuseDialog',
|
||||||
);
|
);
|
||||||
|
@ -809,10 +831,10 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
|
const close = Modal.createTrackedDialog('Chat create or reuse', '', ChatCreateOrReuseDialog, {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
if (!success) {
|
if (!success && goHomeOnCancel) {
|
||||||
// Dialog cancelled, default to home
|
// Dialog cancelled, default to home
|
||||||
dis.dispatch({ action: 'view_home_page' });
|
dis.dispatch({ action: 'view_home_page' });
|
||||||
}
|
}
|
||||||
|
@ -837,7 +859,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_invite: function(roomId) {
|
_invite: function(roomId) {
|
||||||
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
Modal.createDialog(ChatInviteDialog, {
|
Modal.createTrackedDialog('Chat Invite', '', ChatInviteDialog, {
|
||||||
title: _t('Invite new room members'),
|
title: _t('Invite new room members'),
|
||||||
description: _t('Who would you like to add to this room?'),
|
description: _t('Who would you like to add to this room?'),
|
||||||
button: _t('Send Invites'),
|
button: _t('Send Invites'),
|
||||||
|
@ -851,7 +873,7 @@ module.exports = React.createClass({
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||||
title: _t("Leave room"),
|
title: _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
<span>
|
<span>
|
||||||
|
@ -874,7 +896,7 @@ module.exports = React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
console.error("Failed to leave room " + roomId + " " + err);
|
console.error("Failed to leave room " + roomId + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||||
title: _t("Failed to leave room"),
|
title: _t("Failed to leave room"),
|
||||||
description: (err && err.message ? err.message :
|
description: (err && err.message ? err.message :
|
||||||
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
||||||
|
@ -970,14 +992,12 @@ module.exports = React.createClass({
|
||||||
_showScreenAfterLogin: function() {
|
_showScreenAfterLogin: function() {
|
||||||
// If screenAfterLogin is set, use that, then null it so that a second login will
|
// If screenAfterLogin is set, use that, then null it so that a second login will
|
||||||
// result in view_home_page, _user_settings or _room_directory
|
// result in view_home_page, _user_settings or _room_directory
|
||||||
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
|
if (this._screenAfterLogin && this._screenAfterLogin.screen) {
|
||||||
this.showScreen(
|
this.showScreen(
|
||||||
this.state.screenAfterLogin.screen,
|
this._screenAfterLogin.screen,
|
||||||
this.state.screenAfterLogin.params,
|
this._screenAfterLogin.params,
|
||||||
);
|
);
|
||||||
// XXX: is this necessary? `showScreen` should do it for us.
|
this._screenAfterLogin = null;
|
||||||
this.notifyNewScreen(this.state.screenAfterLogin.screen);
|
|
||||||
this.setState({screenAfterLogin: null});
|
|
||||||
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
|
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
|
||||||
// Before defaulting to directory, show the last viewed room
|
// Before defaulting to directory, show the last viewed room
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -1012,16 +1032,12 @@ module.exports = React.createClass({
|
||||||
*/
|
*/
|
||||||
_onWillStartClient() {
|
_onWillStartClient() {
|
||||||
const self = this;
|
const self = this;
|
||||||
// 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.setState({ready: false});
|
|
||||||
|
|
||||||
// reset the 'have completed first sync' flag,
|
// reset the 'have completed first sync' flag,
|
||||||
// since we're about to start the client and therefore about
|
// since we're about to start the client and therefore about
|
||||||
// to do the first sync
|
// to do the first sync
|
||||||
this.firstSyncComplete = false;
|
this.firstSyncComplete = false;
|
||||||
this.firstSyncPromise = q.defer();
|
this.firstSyncPromise = Promise.defer();
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||||
|
@ -1074,7 +1090,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
cli.on('Session.logged_out', function(call) {
|
cli.on('Session.logged_out', function(call) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
|
||||||
title: _t('Signed Out'),
|
title: _t('Signed Out'),
|
||||||
description: _t('For security, this session has been signed out. Please sign in again.'),
|
description: _t('For security, this session has been signed out. Please sign in again.'),
|
||||||
});
|
});
|
||||||
|
@ -1139,6 +1155,10 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_directory',
|
action: 'view_room_directory',
|
||||||
});
|
});
|
||||||
|
} else if (screen == 'groups') {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_my_groups',
|
||||||
|
});
|
||||||
} else if (screen == 'post_registration') {
|
} else if (screen == 'post_registration') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_post_registration',
|
action: 'start_post_registration',
|
||||||
|
@ -1183,21 +1203,33 @@ module.exports = React.createClass({
|
||||||
} else if (screen.indexOf('user/') == 0) {
|
} else if (screen.indexOf('user/') == 0) {
|
||||||
const userId = screen.substring(5);
|
const userId = screen.substring(5);
|
||||||
|
|
||||||
|
// Wait for the first sync so that `getRoom` gives us a room object if it's
|
||||||
|
// in the sync response
|
||||||
|
const waitFor = this.firstSyncPromise ?
|
||||||
|
this.firstSyncPromise.promise : Promise.resolve();
|
||||||
|
waitFor.then(() => {
|
||||||
if (params.action === 'chat') {
|
if (params.action === 'chat') {
|
||||||
this._chatCreateOrReuse(userId);
|
this._chatCreateOrReuse(userId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ viewUserId: userId });
|
|
||||||
this._setPage(PageTypes.UserView);
|
this._setPage(PageTypes.UserView);
|
||||||
this.notifyNewScreen('user/' + userId);
|
this.notifyNewScreen('user/' + userId);
|
||||||
const member = new Matrix.RoomMember(null, userId);
|
const member = new Matrix.RoomMember(null, userId);
|
||||||
if (member) {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
member: member,
|
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 {
|
} else {
|
||||||
console.info("Ignoring showScreen for '%s'", screen);
|
console.info("Ignoring showScreen for '%s'", screen);
|
||||||
}
|
}
|
||||||
|
@ -1226,6 +1258,11 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onGroupClick: function(event, groupId) {
|
||||||
|
event.preventDefault();
|
||||||
|
dis.dispatch({action: 'view_group', group_id: groupId});
|
||||||
|
},
|
||||||
|
|
||||||
onLogoutClick: function(event) {
|
onLogoutClick: function(event) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'logout',
|
action: 'logout',
|
||||||
|
@ -1240,20 +1277,20 @@ module.exports = React.createClass({
|
||||||
const hideRhsThreshold = 820;
|
const hideRhsThreshold = 820;
|
||||||
const showRhsThreshold = 820;
|
const showRhsThreshold = 820;
|
||||||
|
|
||||||
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_right_panel' });
|
dis.dispatch({ action: 'hide_right_panel' });
|
||||||
}
|
}
|
||||||
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
||||||
dis.dispatch({ action: 'show_right_panel' });
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({width: window.innerWidth});
|
this._windowWidth = window.innerWidth;
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomCreated: function(roomId) {
|
onRoomCreated: function(roomId) {
|
||||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require("react-dom");
|
import ReactDOM from 'react-dom';
|
||||||
var dis = require("../../dispatcher");
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
var sdk = require('../../index');
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
import dis from "../../dispatcher";
|
||||||
|
import sdk from '../../index';
|
||||||
|
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
|
||||||
|
@ -90,9 +92,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// show timestamps always
|
// show timestamps always
|
||||||
alwaysShowTimestamps: React.PropTypes.bool,
|
alwaysShowTimestamps: React.PropTypes.bool,
|
||||||
|
|
||||||
// hide redacted events as per old behaviour
|
|
||||||
hideRedactions: React.PropTypes.bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -113,6 +112,8 @@ module.exports = React.createClass({
|
||||||
// Velocity requires
|
// Velocity requires
|
||||||
this._readMarkerGhostNode = null;
|
this._readMarkerGhostNode = null;
|
||||||
|
|
||||||
|
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -238,8 +239,20 @@ module.exports = React.createClass({
|
||||||
return !this._isMounted;
|
return !this._isMounted;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEventTiles: function() {
|
// TODO: Implement granular (per-room) hide options
|
||||||
|
_shouldShowEvent: function(mxEv) {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
|
return false; // no tile = no show
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show highlighted event
|
||||||
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
|
return !shouldHideEvent(mxEv, this._syncedSettings);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getEventTiles: function() {
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
|
@ -249,20 +262,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// first figure out which is the last event in the list which we're
|
// first figure out which is the last event in the list which we're
|
||||||
// actually going to show; this allows us to behave slightly
|
// actually going to show; this allows us to behave slightly
|
||||||
// differently for the last event in the list.
|
// differently for the last event in the list. (eg show timestamp)
|
||||||
//
|
//
|
||||||
// we also need to figure out which is the last event we show which isn't
|
// we also need to figure out which is the last event we show which isn't
|
||||||
// a local echo, to manage the read-marker.
|
// a local echo, to manage the read-marker.
|
||||||
var lastShownEventIndex = -1;
|
let lastShownEvent;
|
||||||
|
|
||||||
var lastShownNonLocalEchoIndex = -1;
|
var lastShownNonLocalEchoIndex = -1;
|
||||||
for (i = this.props.events.length-1; i >= 0; i--) {
|
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||||
var mxEv = this.props.events[i];
|
var mxEv = this.props.events[i];
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!this._shouldShowEvent(mxEv)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastShownEventIndex < 0) {
|
if (lastShownEvent === undefined) {
|
||||||
lastShownEventIndex = i;
|
lastShownEvent = mxEv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
|
@ -288,25 +302,18 @@ module.exports = React.createClass({
|
||||||
this.currentGhostEventId = null;
|
this.currentGhostEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isMembershipChange = (e) => e.getType() === 'm.room.member';
|
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
let mxEv = this.props.events[i];
|
let mxEv = this.props.events[i];
|
||||||
let wantTile = true;
|
|
||||||
let eventId = mxEv.getId();
|
let eventId = mxEv.getId();
|
||||||
let readMarkerInMels = false;
|
let last = (mxEv === lastShownEvent);
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
wantTile = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let last = (i == lastShownEventIndex);
|
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) &&
|
if (isMembershipChange(mxEv) && wantTile) {
|
||||||
EventTile.haveTileForEvent(mxEv) &&
|
let readMarkerInMels = false;
|
||||||
!mxEv.isRedacted()
|
|
||||||
) {
|
|
||||||
let ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
// member events. This will prevent it from being re-created unnecessarily, and
|
// member events. This will prevent it from being re-created unnecessarily, and
|
||||||
|
@ -323,37 +330,43 @@ module.exports = React.createClass({
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If RM event is the first in the MELS, append the RM after MELS
|
||||||
|
if (mxEv.getId() === this.props.readMarkerEventId) {
|
||||||
|
readMarkerInMels = true;
|
||||||
|
}
|
||||||
|
|
||||||
let summarisedEvents = [mxEv];
|
let summarisedEvents = [mxEv];
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
let collapsedMxEv = this.props.events[i + 1];
|
const collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
// Ignore redacted member events
|
|
||||||
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv) ||
|
if (!isMembershipChange(collapsedMxEv) ||
|
||||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
summarisedEvents.push(collapsedMxEv);
|
|
||||||
}
|
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
|
||||||
|
|
||||||
let eventTiles = summarisedEvents.map(
|
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
|
||||||
(e) => {
|
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||||
if (e.getId() === this.props.readMarkerEventId) {
|
|
||||||
readMarkerInMels = true;
|
readMarkerInMels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore redacted/hidden member events
|
||||||
|
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
summarisedEvents.push(collapsedMxEv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
|
let eventTiles = summarisedEvents.map((e) => {
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
// timestamp of the current event, and no DateSeperator is inserted.
|
// timestamp of the current event, and no DateSeperator is inserted.
|
||||||
let ret = this._getTilesForEvent(e, e);
|
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||||
prevEvent = e;
|
prevEvent = e;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}).reduce((a, b) => a.concat(b));
|
||||||
).reduce((a, b) => a.concat(b));
|
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
|
@ -466,8 +479,6 @@ module.exports = React.createClass({
|
||||||
continuation = false;
|
continuation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEv.isRedacted() && this.props.hideRedactions) return ret;
|
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
var eventId = mxEv.getId();
|
||||||
var highlight = (eventId == this.props.highlightedEventId);
|
var highlight = (eventId == this.props.highlightedEventId);
|
||||||
|
|
||||||
|
|
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.createTrackedDialog('Create Group', '', 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.
|
// the room this statusbar is representing.
|
||||||
room: React.PropTypes.object.isRequired,
|
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
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: React.PropTypes.number,
|
||||||
|
|
||||||
|
@ -143,12 +140,9 @@ module.exports = React.createClass({
|
||||||
(this.state.usersTyping.length > 0) ||
|
(this.state.usersTyping.length > 0) ||
|
||||||
this.props.numUnreadMessages ||
|
this.props.numUnreadMessages ||
|
||||||
!this.props.atEndOfLiveTimeline ||
|
!this.props.atEndOfLiveTimeline ||
|
||||||
this.props.hasActiveCall ||
|
this.props.hasActiveCall
|
||||||
this.props.tabComplete.isTabCompleting()
|
|
||||||
) {
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.props.tabCompleteEntries) {
|
|
||||||
return STATUS_BAR_HIDDEN;
|
|
||||||
} else if (this.props.unsentMessageError) {
|
} else if (this.props.unsentMessageError) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
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.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent: function() {
|
_getContent: function() {
|
||||||
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
|
||||||
// no conn bar trumps unread count since you can't get unread messages
|
// 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) {
|
if (this.props.unsentMessageError) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
|
|
|
@ -22,7 +22,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
@ -33,7 +33,6 @@ var ContentMessages = require("../../ContentMessages");
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var CallHandler = require('../../CallHandler');
|
var CallHandler = require('../../CallHandler');
|
||||||
var TabComplete = require("../../TabComplete");
|
|
||||||
var Resend = require("../../Resend");
|
var Resend = require("../../Resend");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
var Tinter = require("../../Tinter");
|
var Tinter = require("../../Tinter");
|
||||||
|
@ -47,13 +46,14 @@ import UserProvider from '../../autocomplete/UserProvider';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
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) {
|
if (DEBUG) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
debuglog = console.log.bind(console);
|
||||||
} else {
|
|
||||||
var debuglog = function() {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -113,6 +113,7 @@ module.exports = React.createClass({
|
||||||
callState: null,
|
callState: null,
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
|
showApps: false,
|
||||||
|
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
|
@ -142,15 +143,6 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
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
|
// Start listening for RoomViewStore updates
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._onRoomViewStoreUpdate(true);
|
this._onRoomViewStoreUpdate(true);
|
||||||
|
@ -234,10 +226,9 @@ module.exports = React.createClass({
|
||||||
// making it impossible to indicate a newly joined room.
|
// making it impossible to indicate a newly joined room.
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (room) {
|
if (room) {
|
||||||
this._updateAutoComplete(room);
|
|
||||||
this.tabComplete.loadEntries(room);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
unsentMessageError: this._getUnsentMessageError(room),
|
unsentMessageError: this._getUnsentMessageError(room),
|
||||||
|
showApps: this._shouldShowApps(room),
|
||||||
});
|
});
|
||||||
this._onRoomLoaded(room);
|
this._onRoomLoaded(room);
|
||||||
}
|
}
|
||||||
|
@ -275,6 +266,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_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() {
|
componentDidMount: function() {
|
||||||
var call = this._getCallForRoom();
|
var call = this._getCallForRoom();
|
||||||
var callState = call ? call.call_state : "ended";
|
var callState = call ? call.call_state : "ended";
|
||||||
|
@ -455,9 +459,14 @@ module.exports = React.createClass({
|
||||||
this._updateConfCallNotification();
|
this._updateConfCallNotification();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
callState: callState
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'appsDrawer':
|
||||||
|
this.setState({
|
||||||
|
showApps: payload.show,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -499,9 +508,7 @@ module.exports = React.createClass({
|
||||||
// update the tab complete list as it depends on who most recently spoke,
|
// update the tab complete list as it depends on who most recently spoke,
|
||||||
// and that has probably just changed
|
// and that has probably just changed
|
||||||
if (ev.sender) {
|
if (ev.sender) {
|
||||||
this.tabComplete.onMemberSpoke(ev.sender);
|
UserProvider.getInstance().onUserSpoke(ev.sender);
|
||||||
// nb. we don't need to update the new autocomplete here since
|
|
||||||
// its results are currently ordered purely by search score.
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -524,6 +531,7 @@ module.exports = React.createClass({
|
||||||
this._warnAboutEncryption(room);
|
this._warnAboutEncryption(room);
|
||||||
this._calculatePeekRules(room);
|
this._calculatePeekRules(room);
|
||||||
this._updatePreviewUrlVisibility(room);
|
this._updatePreviewUrlVisibility(room);
|
||||||
|
UserProvider.getInstance().setUserListFromRoom(room);
|
||||||
},
|
},
|
||||||
|
|
||||||
_warnAboutEncryption: function(room) {
|
_warnAboutEncryption: function(room) {
|
||||||
|
@ -536,7 +544,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (!userHasUsedEncryption) {
|
if (!userHasUsedEncryption) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('E2E Warning', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
hasCancelButton: false,
|
hasCancelButton: false,
|
||||||
description: (
|
description: (
|
||||||
|
@ -699,8 +707,7 @@ module.exports = React.createClass({
|
||||||
this._updateConfCallNotification();
|
this._updateConfCallNotification();
|
||||||
|
|
||||||
// refresh the tab complete list
|
// refresh the tab complete list
|
||||||
this.tabComplete.loadEntries(this.state.room);
|
UserProvider.getInstance().setUserListFromRoom(this.state.room);
|
||||||
this._updateAutoComplete(this.state.room);
|
|
||||||
|
|
||||||
// if we are now a member of the room, where we were not before, that
|
// 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
|
// means we have finished joining a room we were previously peeking
|
||||||
|
@ -768,7 +775,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
if (!backwards) {
|
if (!backwards) {
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.searchResults.next_batch) {
|
if (this.state.searchResults.next_batch) {
|
||||||
|
@ -778,7 +785,7 @@ module.exports = React.createClass({
|
||||||
return this._handleSearchResult(searchPromise);
|
return this._handleSearchResult(searchPromise);
|
||||||
} else {
|
} else {
|
||||||
debuglog("no more search results");
|
debuglog("no more search results");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -813,7 +820,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||||
const close = Modal.createDialog(SetMxIdDialog, {
|
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||||
homeserverUrl: cli.getHomeserverUrl(),
|
homeserverUrl: cli.getHomeserverUrl(),
|
||||||
onFinished: (submitted, credentials) => {
|
onFinished: (submitted, credentials) => {
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
|
@ -839,7 +846,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
q().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const signUrl = this.props.thirdPartyInvite ?
|
const signUrl = this.props.thirdPartyInvite ?
|
||||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -858,7 +865,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return q();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -927,7 +934,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to upload file " + file + " " + error);
|
console.error("Failed to upload file " + file + " " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
||||||
title: _t('Failed to upload file'),
|
title: _t('Failed to upload file'),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||||
});
|
});
|
||||||
|
@ -1014,7 +1021,7 @@ module.exports = React.createClass({
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Search failed: " + error);
|
console.error("Search failed: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
|
||||||
title: _t("Search failed"),
|
title: _t("Search failed"),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
|
||||||
});
|
});
|
||||||
|
@ -1141,7 +1148,7 @@ module.exports = React.createClass({
|
||||||
console.error(result.reason);
|
console.error(result.reason);
|
||||||
});
|
});
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
|
||||||
title: _t("Failed to save settings"),
|
title: _t("Failed to save settings"),
|
||||||
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
||||||
});
|
});
|
||||||
|
@ -1188,7 +1195,7 @@ module.exports = React.createClass({
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
var errCode = err.errcode || _t("unknown error code");
|
var errCode = err.errcode || _t("unknown error code");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
||||||
});
|
});
|
||||||
|
@ -1210,7 +1217,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var msg = error.message ? error.message : JSON.stringify(error);
|
var msg = error.message ? error.message : JSON.stringify(error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to reject invite"),
|
title: _t("Failed to reject invite"),
|
||||||
description: msg,
|
description: msg,
|
||||||
});
|
});
|
||||||
|
@ -1425,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() {
|
render: function() {
|
||||||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
|
@ -1560,7 +1559,6 @@ module.exports = React.createClass({
|
||||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
tabComplete={this.tabComplete}
|
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
unsentMessageError={this.state.unsentMessageError}
|
unsentMessageError={this.state.unsentMessageError}
|
||||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||||
|
@ -1613,11 +1611,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var auxPanel = (
|
var auxPanel = (
|
||||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||||
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
conferenceHandler={this.props.ConferenceHandler}
|
conferenceHandler={this.props.ConferenceHandler}
|
||||||
draggingFile={this.state.draggingFile}
|
draggingFile={this.state.draggingFile}
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
onResize={this.onChildResize} >
|
onResize={this.onChildResize}
|
||||||
|
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
||||||
{ aux }
|
{ aux }
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
@ -1630,8 +1630,13 @@ module.exports = React.createClass({
|
||||||
if (canSpeak) {
|
if (canSpeak) {
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
room={this.state.room}
|
||||||
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
|
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
|
// 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 React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var KeyCode = require('../../KeyCode');
|
var KeyCode = require('../../KeyCode');
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
stickyBottom: true,
|
stickyBottom: true,
|
||||||
startAtBottom: true,
|
startAtBottom: true,
|
||||||
onFillRequest: function(backwards) { return q(false); },
|
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||||
onUnfillRequest: function(backwards, scrollToken) {},
|
onUnfillRequest: function(backwards, scrollToken) {},
|
||||||
onScroll: function() {},
|
onScroll: function() {},
|
||||||
};
|
};
|
||||||
|
@ -386,19 +386,12 @@ module.exports = React.createClass({
|
||||||
debuglog("ScrollPanel: starting "+dir+" fill");
|
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||||
|
|
||||||
// onFillRequest can end up calling us recursively (via onScroll
|
// onFillRequest can end up calling us recursively (via onScroll
|
||||||
// events) so make sure we set this before firing off the call. That
|
// events) so make sure we set this before firing off the call.
|
||||||
// does present the risk that we might not ever actually fire off the
|
|
||||||
// fill request, so wrap it in a try/catch.
|
|
||||||
this._pendingFillRequests[dir] = true;
|
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;
|
this._pendingFillRequests[dir] = false;
|
||||||
}).then((hasMoreResults) => {
|
}).then((hasMoreResults) => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
var EventTimeline = Matrix.EventTimeline;
|
var EventTimeline = Matrix.EventTimeline;
|
||||||
|
@ -181,9 +181,6 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// always show timestamps on event tiles?
|
// always show timestamps on event tiles?
|
||||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||||
|
|
||||||
// hide redacted events as per old behaviour
|
|
||||||
hideRedactions: syncedSettings.hideRedactions,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -311,13 +308,13 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
if (!this.state[canPaginateKey]) {
|
if (!this.state[canPaginateKey]) {
|
||||||
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this._timelineWindow.canPaginate(dir)) {
|
if(!this._timelineWindow.canPaginate(dir)) {
|
||||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||||
this.setState({[canPaginateKey]: false});
|
this.setState({[canPaginateKey]: false});
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||||
|
@ -350,9 +347,9 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function() {
|
onMessageListScroll: function(e) {
|
||||||
if (this.props.onScroll) {
|
if (this.props.onScroll) {
|
||||||
this.props.onScroll();
|
this.props.onScroll(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
|
@ -926,7 +923,7 @@ var TimelinePanel = React.createClass({
|
||||||
var message = (error.errcode == 'M_FORBIDDEN')
|
var message = (error.errcode == 'M_FORBIDDEN')
|
||||||
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
||||||
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||||
title: _t("Failed to load timeline position"),
|
title: _t("Failed to load timeline position"),
|
||||||
description: message,
|
description: message,
|
||||||
onFinished: onFinished,
|
onFinished: onFinished,
|
||||||
|
@ -1122,7 +1119,6 @@ var TimelinePanel = React.createClass({
|
||||||
return (
|
return (
|
||||||
<MessagePanel ref="messagePanel"
|
<MessagePanel ref="messagePanel"
|
||||||
hidden={ this.props.hidden }
|
hidden={ this.props.hidden }
|
||||||
hideRedactions={ this.state.hideRedactions }
|
|
||||||
backPaginating={ this.state.backPaginating }
|
backPaginating={ this.state.backPaginating }
|
||||||
forwardPaginating={ forwardPaginating }
|
forwardPaginating={ forwardPaginating }
|
||||||
events={ this.state.events }
|
events={ this.state.events }
|
||||||
|
|
|
@ -22,7 +22,7 @@ const PlatformPeg = require("../../PlatformPeg");
|
||||||
const Modal = require('../../Modal');
|
const Modal = require('../../Modal');
|
||||||
const dis = require("../../dispatcher");
|
const dis = require("../../dispatcher");
|
||||||
import sessionStore from '../../stores/SessionStore';
|
import sessionStore from '../../stores/SessionStore';
|
||||||
const q = require('q');
|
import Promise from 'bluebird';
|
||||||
const packageJson = require('../../../package.json');
|
const packageJson = require('../../../package.json');
|
||||||
const UserSettingsStore = require('../../UserSettingsStore');
|
const UserSettingsStore = require('../../UserSettingsStore');
|
||||||
const CallMediaHandler = require('../../CallMediaHandler');
|
const CallMediaHandler = require('../../CallMediaHandler');
|
||||||
|
@ -81,6 +81,14 @@ const SETTINGS_LABELS = [
|
||||||
id: 'showTwelveHourTimestamps',
|
id: 'showTwelveHourTimestamps',
|
||||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'hideJoinLeaves',
|
||||||
|
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideAvatarDisplaynameChanges',
|
||||||
|
label: 'Hide avatar and display name changes',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'useCompactLayout',
|
id: 'useCompactLayout',
|
||||||
label: 'Use compact timeline layout',
|
label: 'Use compact timeline layout',
|
||||||
|
@ -90,8 +98,16 @@ const SETTINGS_LABELS = [
|
||||||
label: 'Hide removed messages',
|
label: 'Hide removed messages',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'disableMarkdown',
|
id: 'enableSyntaxHighlightLanguageDetection',
|
||||||
label: 'Disable markdown formatting',
|
label: 'Enable automatic language detection for syntax highlighting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||||
|
label: 'Automatically replace plain text Emoji',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Pill.shouldHidePillAvatar',
|
||||||
|
label: 'Hide avatars in user and room mentions',
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
|
@ -199,7 +215,7 @@ module.exports = React.createClass({
|
||||||
this._addThreepid = null;
|
this._addThreepid = null;
|
||||||
|
|
||||||
if (PlatformPeg.get()) {
|
if (PlatformPeg.get()) {
|
||||||
q().then(() => {
|
Promise.resolve().then(() => {
|
||||||
return PlatformPeg.get().getAppVersion();
|
return PlatformPeg.get().getAppVersion();
|
||||||
}).done((appVersion) => {
|
}).done((appVersion) => {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
|
@ -297,7 +313,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshMediaDevices: function() {
|
_refreshMediaDevices: function() {
|
||||||
q().then(() => {
|
Promise.resolve().then(() => {
|
||||||
return CallMediaHandler.getDevices();
|
return CallMediaHandler.getDevices();
|
||||||
}).then((mediaDevices) => {
|
}).then((mediaDevices) => {
|
||||||
// console.log("got mediaDevices", mediaDevices, this._unmounted);
|
// console.log("got mediaDevices", mediaDevices, this._unmounted);
|
||||||
|
@ -312,7 +328,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_refreshFromServer: function() {
|
_refreshFromServer: function() {
|
||||||
const self = this;
|
const self = this;
|
||||||
q.all([
|
Promise.all([
|
||||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
|
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
|
||||||
]).done(function(resps) {
|
]).done(function(resps) {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -323,7 +339,7 @@ module.exports = React.createClass({
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to load user settings: " + error);
|
console.error("Failed to load user settings: " + error);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, {
|
||||||
title: _t("Can't load user settings"),
|
title: _t("Can't load user settings"),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
|
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
|
||||||
});
|
});
|
||||||
|
@ -356,7 +372,7 @@ module.exports = React.createClass({
|
||||||
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
// const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
console.error("Failed to set avatar: " + err);
|
console.error("Failed to set avatar: " + err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
|
||||||
title: _t("Failed to set avatar."),
|
title: _t("Failed to set avatar."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -365,7 +381,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
||||||
title: _t("Sign out"),
|
title: _t("Sign out"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
@ -401,7 +417,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to change password: " + errMsg);
|
console.error("Failed to change password: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: errMsg,
|
description: errMsg,
|
||||||
});
|
});
|
||||||
|
@ -409,7 +425,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onPasswordChanged: function() {
|
onPasswordChanged: function() {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||||
title: _t("Success"),
|
title: _t("Success"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Your password was successfully changed. You will not receive " +
|
"Your password was successfully changed. You will not receive " +
|
||||||
|
@ -434,7 +450,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const emailAddress = this.refs.add_email_input.value;
|
const emailAddress = this.refs.add_email_input.value;
|
||||||
if (!Email.looksValid(emailAddress)) {
|
if (!Email.looksValid(emailAddress)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
|
||||||
title: _t("Invalid Email Address"),
|
title: _t("Invalid Email Address"),
|
||||||
description: _t("This doesn't appear to be a valid email address"),
|
description: _t("This doesn't appear to be a valid email address"),
|
||||||
});
|
});
|
||||||
|
@ -444,7 +460,7 @@ module.exports = React.createClass({
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Please check your email and click on the link it contains. Once this " +
|
"Please check your email and click on the link it contains. Once this " +
|
||||||
|
@ -456,7 +472,7 @@ module.exports = React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
console.error("Unable to add email address " + emailAddress + " " + err);
|
console.error("Unable to add email address " + emailAddress + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to add email address"),
|
title: _t("Unable to add email address"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -467,7 +483,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRemoveThreepidClicked: function(threepid) {
|
onRemoveThreepidClicked: function(threepid) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, {
|
||||||
title: _t("Remove Contact Information?"),
|
title: _t("Remove Contact Information?"),
|
||||||
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
|
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
|
||||||
button: _t('Remove'),
|
button: _t('Remove'),
|
||||||
|
@ -481,7 +497,7 @@ module.exports = React.createClass({
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -513,7 +529,7 @@ module.exports = React.createClass({
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const message = _t("Unable to verify email address.") + " " +
|
const message = _t("Unable to verify email address.") + " " +
|
||||||
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: message,
|
description: message,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
|
@ -522,7 +538,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to verify email address: " + err);
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify email address."),
|
title: _t("Unable to verify email address."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -532,7 +548,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onDeactivateAccountClicked: function() {
|
_onDeactivateAccountClicked: function() {
|
||||||
const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
|
const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
|
||||||
Modal.createDialog(DeactivateAccountDialog, {});
|
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onBugReportClicked: function() {
|
_onBugReportClicked: function() {
|
||||||
|
@ -540,7 +556,7 @@ module.exports = React.createClass({
|
||||||
if (!BugReportDialog) {
|
if (!BugReportDialog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Modal.createDialog(BugReportDialog, {});
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onClearCacheClicked: function() {
|
_onClearCacheClicked: function() {
|
||||||
|
@ -564,39 +580,36 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
// reject the invites
|
// reject the invites
|
||||||
const promises = rooms.map((room) => {
|
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
|
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||||
// after trying to reject all the invites.
|
// after trying to reject all the invites.
|
||||||
q.allSettled(promises).then(() => {
|
});
|
||||||
|
});
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
rejectingInvites: false,
|
rejectingInvites: false,
|
||||||
});
|
});
|
||||||
}).done();
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onExportE2eKeysClicked: function() {
|
_onExportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
|
||||||
(cb) => {
|
|
||||||
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
}, "e2e-export");
|
}, "e2e-export");
|
||||||
}, {
|
}, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onImportE2eKeysClicked: function() {
|
_onImportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
|
||||||
(cb) => {
|
|
||||||
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||||
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||||
}, "e2e-export");
|
}, "e2e-export");
|
||||||
}, {
|
}, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderReferral: function() {
|
_renderReferral: function() {
|
||||||
|
@ -642,6 +655,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("User Interface") }</h3>
|
<h3>{ _t("User Interface") }</h3>
|
||||||
|
@ -649,8 +666,21 @@ module.exports = React.createClass({
|
||||||
{ this._renderUrlPreviewSelector() }
|
{ this._renderUrlPreviewSelector() }
|
||||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||||
{ THEMES.map( this._renderThemeSelector ) }
|
{ 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() }
|
{ this._renderLanguageSetting() }
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -829,7 +859,13 @@ module.exports = React.createClass({
|
||||||
if (this.props.enableLabs === false) return null;
|
if (this.props.enableLabs === false) return null;
|
||||||
UserSettingsStore.doTranslations();
|
UserSettingsStore.doTranslations();
|
||||||
|
|
||||||
const features = UserSettingsStore.LABS_FEATURES.map((feature) => {
|
const features = [];
|
||||||
|
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
|
||||||
|
// This feature has an override and will be set to the default, so do not
|
||||||
|
// show it here.
|
||||||
|
if (feature.override) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
|
@ -837,7 +873,7 @@ module.exports = React.createClass({
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
features.push(
|
||||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -847,9 +883,14 @@ module.exports = React.createClass({
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
/>
|
/>
|
||||||
<label htmlFor={feature.id}>{feature.name}</label>
|
<label htmlFor={feature.id}>{feature.name}</label>
|
||||||
</div>
|
</div>);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No labs section when there are no features in labs
|
||||||
|
if (features.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("Labs") }</h3>
|
<h3>{ _t("Labs") }</h3>
|
||||||
|
@ -978,7 +1019,7 @@ module.exports = React.createClass({
|
||||||
this._refreshMediaDevices,
|
this._refreshMediaDevices,
|
||||||
function() {
|
function() {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||||
title: _t('No media permissions'),
|
title: _t('No media permissions'),
|
||||||
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
||||||
});
|
});
|
||||||
|
@ -1114,7 +1155,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
||||||
const id = "3pid-" + val.address;
|
const id = "3pid-" + val.address;
|
||||||
// TODO; make a separate component to avoid having to rebind onClick
|
// TODO: make a separate component to avoid having to rebind onClick
|
||||||
// each time we render
|
// each time we render
|
||||||
const onRemoveClick = (e) => this.onRemoveThreepidClicked(val);
|
const onRemoveClick = (e) => this.onRemoveThreepidClicked(val);
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
@ -121,15 +121,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onExportE2eKeysClicked: function() {
|
_onExportE2eKeysClicked: function() {
|
||||||
Modal.createDialogAsync(
|
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => {
|
||||||
(cb) => {
|
|
||||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
}, "e2e-export");
|
}, "e2e-export");
|
||||||
}, {
|
}, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputChanged: function(stateKey, ev) {
|
onInputChanged: function(stateKey, ev) {
|
||||||
|
@ -152,7 +150,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
showErrorDialog: function(body, title) {
|
showErrorDialog: function(body, title) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||||
title: title,
|
title: title,
|
||||||
description: body,
|
description: body,
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,8 +19,11 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t, _tJsx } from '../../../languageHandler';
|
||||||
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||||
|
@ -72,9 +75,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this._initLoginLogic();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -87,6 +95,9 @@ module.exports = React.createClass({
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
this.props.onLoggedIn(data);
|
this.props.onLoggedIn(data);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
|
if(this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let errorText;
|
let errorText;
|
||||||
|
|
||||||
// Some error strings only apply for logging in
|
// Some error strings only apply for logging in
|
||||||
|
@ -109,8 +120,11 @@ module.exports = React.createClass({
|
||||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
if(this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false
|
busy: false,
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -295,6 +309,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onLanguageChange: function(newLang) {
|
||||||
|
if(languageHandler.getCurrentLanguage() !== newLang) {
|
||||||
|
UserSettingsStore.setLocalSetting('language', newLang);
|
||||||
|
PlatformPeg.get().reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderLanguageSetting: function() {
|
||||||
|
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||||
|
return <div className="mx_Login_language_div">
|
||||||
|
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
||||||
|
className="mx_Login_language"
|
||||||
|
value={languageHandler.getCurrentLanguage()}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
|
@ -343,6 +374,7 @@ module.exports = React.createClass({
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
|
{ this._renderLanguageSetting() }
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
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
|
// 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
|
// that the user registered with at this stage (depending on whether this
|
||||||
// is the client they initiated registration).
|
// is the client they initiated registration).
|
||||||
let trackPromise = q(null);
|
let trackPromise = Promise.resolve(null);
|
||||||
if (this._rtsClient && extra.emailSid) {
|
if (this._rtsClient && extra.emailSid) {
|
||||||
// Track referral if this.props.referrer set, get team_token in order to
|
// Track referral if this.props.referrer set, get team_token in order to
|
||||||
// retrieve team config and see welcome page etc.
|
// retrieve team config and see welcome page etc.
|
||||||
|
@ -232,7 +232,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_setupPushers: function(matrixClient) {
|
_setupPushers: function(matrixClient) {
|
||||||
if (!this.props.brand) {
|
if (!this.props.brand) {
|
||||||
return q();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return matrixClient.getPushers().then((resp)=>{
|
return matrixClient.getPushers().then((resp)=>{
|
||||||
const pushers = resp.pushers;
|
const pushers = resp.pushers;
|
||||||
|
|
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) {
|
getRoomAvatarUrl: function(props) {
|
||||||
if (!this.props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
return props.room.getAvatarUrl(
|
return props.room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
@ -84,7 +84,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getOneToOneAvatar: function(props) {
|
getOneToOneAvatar: function(props) {
|
||||||
if (!this.props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
var mlist = props.room.currentState.members;
|
var mlist = props.room.currentState.members;
|
||||||
var userIds = [];
|
var userIds = [];
|
||||||
|
@ -126,9 +126,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getFallbackAvatar: function(props) {
|
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() {
|
render: function() {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
||||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||||
"views.dialogs.ChatCreateOrReuseDialog",
|
"views.dialogs.ChatCreateOrReuseDialog",
|
||||||
);
|
);
|
||||||
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
|
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
this.props.onFinished(success);
|
this.props.onFinished(success);
|
||||||
|
@ -367,7 +367,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite"),
|
title: _t("Failed to invite"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -380,7 +380,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite user"),
|
title: _t("Failed to invite user"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -401,7 +401,7 @@ module.exports = React.createClass({
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite"),
|
title: _t("Failed to invite"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -448,7 +448,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
});
|
||||||
|
@ -498,7 +498,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a bit to let the user finish typing
|
// wait a bit to let the user finish typing
|
||||||
return q.delay(500).then(() => {
|
return Promise.delay(500).then(() => {
|
||||||
if (cancelled) return null;
|
if (cancelled) return null;
|
||||||
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||||
}).then((res) => {
|
}).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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import Velocity from 'velocity-vector';
|
import Velocity from 'velocity-vector';
|
||||||
|
@ -54,6 +55,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
user: MatrixClientPeg.get().credentials.userId,
|
user: MatrixClientPeg.get().credentials.userId,
|
||||||
password: this._passwordField.value,
|
password: this._passwordField.value,
|
||||||
}).done(() => {
|
}).done(() => {
|
||||||
|
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||||
Lifecycle.onLoggedOut();
|
Lifecycle.onLoggedOut();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Usage:
|
* Usage:
|
||||||
* Modal.createDialog(ErrorDialog, {
|
* Modal.createTrackedDialog('An Identifier', 'some detail', ErrorDialog, {
|
||||||
* title: "some text", (default: "Error")
|
* title: "some text", (default: "Error")
|
||||||
* description: "some more text",
|
* description: "some more text",
|
||||||
* button: "Button Text",
|
* button: "Button Text",
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default React.createClass({
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
|
|
||||||
console.log("KeyShareDialog: Starting verify dialog");
|
console.log("KeyShareDialog: Starting verify dialog");
|
||||||
Modal.createDialog(DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
device: this.state.deviceInfo,
|
device: this.state.deviceInfo,
|
||||||
onFinished: (verified) => {
|
onFinished: (verified) => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_sendBugReport: function() {
|
_sendBugReport: function() {
|
||||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||||
Modal.createDialog(BugReportDialog, {});
|
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_continueClicked: function() {
|
_continueClicked: function() {
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default React.createClass({
|
||||||
|
|
||||||
const emailAddress = this.state.emailAddress;
|
const emailAddress = this.state.emailAddress;
|
||||||
if (!Email.looksValid(emailAddress)) {
|
if (!Email.looksValid(emailAddress)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
|
||||||
title: _t("Invalid Email Address"),
|
title: _t("Invalid Email Address"),
|
||||||
description: _t("This doesn't appear to be a valid email address"),
|
description: _t("This doesn't appear to be a valid email address"),
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,7 @@ export default React.createClass({
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: _t(
|
description: _t(
|
||||||
"Please check your email and click on the link it contains. Once this " +
|
"Please check your email and click on the link it contains. Once this " +
|
||||||
|
@ -77,7 +77,7 @@ export default React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({emailBusy: false});
|
this.setState({emailBusy: false});
|
||||||
console.error("Unable to add email address " + emailAddress + " " + err);
|
console.error("Unable to add email address " + emailAddress + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to add email address"),
|
title: _t("Unable to add email address"),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
@ -106,7 +106,7 @@ export default React.createClass({
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const message = _t("Unable to verify email address.") + " " +
|
const message = _t("Unable to verify email address.") + " " +
|
||||||
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
|
||||||
title: _t("Verification Pending"),
|
title: _t("Verification Pending"),
|
||||||
description: message,
|
description: message,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
|
@ -115,7 +115,7 @@ export default React.createClass({
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Unable to verify email address: " + err);
|
console.error("Unable to verify email address: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify email address."),
|
title: _t("Unable to verify email address."),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
@ -106,6 +106,16 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_doUsernameCheck: function() {
|
_doUsernameCheck: function() {
|
||||||
|
// XXX: SPEC-1
|
||||||
|
// Check if username is valid
|
||||||
|
// Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
|
||||||
|
if (encodeURIComponent(this.state.username) !== this.state.username) {
|
||||||
|
this.setState({
|
||||||
|
usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if username is available
|
// Check if username is available
|
||||||
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
||||||
(isAvailable) => {
|
(isAvailable) => {
|
||||||
|
@ -242,7 +252,7 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_SetMxIdDialog"
|
<BaseDialog className="mx_SetMxIdDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title="To get started, please pick a username!"
|
title={_t('To get started, please pick a username!')}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_SetMxIdDialog_input_group">
|
<div className="mx_SetMxIdDialog_input_group">
|
||||||
|
|
75
src/components/views/elements/AppPermission.js
Normal file
75
src/components/views/elements/AppPermission.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import url from 'url';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default class AppPermission extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const curlBase = this.getCurlBase();
|
||||||
|
this.state = { curlBase: curlBase};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return string representation of content URL without query parameters
|
||||||
|
getCurlBase() {
|
||||||
|
const wurl = url.parse(this.props.url);
|
||||||
|
let curl;
|
||||||
|
let curlString;
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(wurl.search);
|
||||||
|
|
||||||
|
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||||
|
curl = url.parse(searchParams.get('url'));
|
||||||
|
if(curl) {
|
||||||
|
curl.search = curl.query = "";
|
||||||
|
curlString = curl.format();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!curl && wurl) {
|
||||||
|
wurl.search = wurl.query = "";
|
||||||
|
curlString = wurl.format();
|
||||||
|
}
|
||||||
|
return curlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
isScalarWurl(wurl) {
|
||||||
|
if(wurl && wurl.hostname && (
|
||||||
|
wurl.hostname === 'scalar.vector.im' ||
|
||||||
|
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||||
|
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||||
|
wurl.hostname === 'demo.riot.im' ||
|
||||||
|
wurl.hostname === 'localhost'
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='mx_AppPermissionWarning'>
|
||||||
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
|
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||||
|
</div>
|
||||||
|
<div className='mx_AppPermissionWarningText'>
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>Do you want to load widget from URL:</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className='mx_AppPermissionButton'
|
||||||
|
type='button'
|
||||||
|
value={_t('Allow')}
|
||||||
|
onClick={this.props.onPermissionGranted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPermission.propTypes = {
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
onPermissionGranted: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
AppPermission.defaultProps = {
|
||||||
|
onPermissionGranted: function() {},
|
||||||
|
};
|
284
src/components/views/elements/AppTile.js
Normal file
284
src/components/views/elements/AppTile.js
Normal file
|
@ -0,0 +1,284 @@
|
||||||
|
/*
|
||||||
|
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';
|
||||||
|
import AppPermission from './AppPermission';
|
||||||
|
import AppWarning from './AppWarning';
|
||||||
|
import MessageSpinner from './MessageSpinner';
|
||||||
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
|
||||||
|
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,
|
||||||
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
|
fullWidth: React.PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
|
||||||
|
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
widgetUrl: this.props.url,
|
||||||
|
widgetPermissionId: widgetPermissionId,
|
||||||
|
hasPermissionToLoad: Boolean(hasPermissionToLoad === 'true'),
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
isMixedContent: function() {
|
||||||
|
const parentContentProtocol = window.location.protocol;
|
||||||
|
const u = url.parse(this.props.url);
|
||||||
|
const childContentProtocol = u.protocol;
|
||||||
|
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
||||||
|
console.warn("Refusing to load mixed-content app:",
|
||||||
|
parentContentProtocol, childContentProtocol, window.location, this.props.url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
this._scalarClient.scalarToken = token;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_canUserModify: function() {
|
||||||
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
|
},
|
||||||
|
|
||||||
|
_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.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
},
|
||||||
|
|
||||||
|
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
|
||||||
|
*/
|
||||||
|
_onDeleteClick: function() {
|
||||||
|
if (this._canUserModify()) {
|
||||||
|
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});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Revoke widget permissions - %s", this.props.id);
|
||||||
|
this._revokeWidgetPermission();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Widget labels to render, depending upon user permissions
|
||||||
|
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||||
|
_deleteWidgetLabel() {
|
||||||
|
if (this._canUserModify()) {
|
||||||
|
return 'Delete widget';
|
||||||
|
}
|
||||||
|
return 'Revoke widget access';
|
||||||
|
},
|
||||||
|
|
||||||
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
|
_grantWidgetPermission() {
|
||||||
|
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||||
|
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||||
|
this.setState({hasPermissionToLoad: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
_revokeWidgetPermission() {
|
||||||
|
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||||
|
localStorage.removeItem(this.state.widgetPermissionId);
|
||||||
|
this.setState({hasPermissionToLoad: 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 allow-presentation";
|
||||||
|
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||||
|
let safeWidgetUrl = '';
|
||||||
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.loading) {
|
||||||
|
appTileBody = (
|
||||||
|
<div className='mx_AppTileBody mx_AppLoading'>
|
||||||
|
<MessageSpinner msg='Loading...'/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (this.state.hasPermissionToLoad == true) {
|
||||||
|
if (this.isMixedContent()) {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<AppWarning
|
||||||
|
errorMsg="Error - Mixed content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<iframe
|
||||||
|
ref="appFrame"
|
||||||
|
src={safeWidgetUrl}
|
||||||
|
allowFullScreen="true"
|
||||||
|
sandbox={sandboxFlags}
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<AppPermission
|
||||||
|
url={this.state.widgetUrl}
|
||||||
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// editing is done in scalar
|
||||||
|
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||||
|
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||||
|
let deleteIcon = 'img/cancel.svg';
|
||||||
|
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||||
|
if(this._canUserModify()) {
|
||||||
|
deleteIcon = 'img/cancel-red.svg';
|
||||||
|
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||||
|
}
|
||||||
|
|
||||||
|
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={_t('Edit')}
|
||||||
|
title={_t('Edit')}
|
||||||
|
onClick={this._onEditClick}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
{/* Delete widget */}
|
||||||
|
<img src={deleteIcon}
|
||||||
|
className={deleteClasses}
|
||||||
|
width="8" height="8"
|
||||||
|
alt={_t(deleteWidgetLabel)}
|
||||||
|
title={_t(deleteWidgetLabel)}
|
||||||
|
onClick={this._onDeleteClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{appTileBody}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
25
src/components/views/elements/AppWarning.js
Normal file
25
src/components/views/elements/AppWarning.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
const AppWarning = (props) => {
|
||||||
|
return (
|
||||||
|
<div className='mx_AppPermissionWarning'>
|
||||||
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
|
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||||
|
</div>
|
||||||
|
<div className='mx_AppPermissionWarningText'>
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AppWarning.propTypes = {
|
||||||
|
errorMsg: PropTypes.string,
|
||||||
|
};
|
||||||
|
AppWarning.defaultProps = {
|
||||||
|
errorMsg: 'Error',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppWarning;
|
|
@ -52,7 +52,7 @@ export default React.createClass({
|
||||||
|
|
||||||
onVerifyClick: function() {
|
onVerifyClick: function() {
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
Modal.createDialog(DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
device: this.state.device,
|
device: this.state.device,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component which wraps an EditableText, with a spinner while updates take
|
* A component which wraps an EditableText, with a spinner while updates take
|
||||||
|
@ -148,5 +148,5 @@ EditableTextContainer.defaultProps = {
|
||||||
initialValue: "",
|
initialValue: "",
|
||||||
placeholder: "",
|
placeholder: "",
|
||||||
blurToSubmit: false,
|
blurToSubmit: false,
|
||||||
onSubmit: function(v) {return q(); },
|
onSubmit: function(v) {return Promise.resolve(); },
|
||||||
};
|
};
|
||||||
|
|
34
src/components/views/elements/MessageSpinner.js
Normal file
34
src/components/views/elements/MessageSpinner.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MessageSpinner',
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const w = this.props.w || 32;
|
||||||
|
const h = this.props.h || 32;
|
||||||
|
const imgClass = this.props.imgClassName || "";
|
||||||
|
const msg = this.props.msg || "Loading...";
|
||||||
|
return (
|
||||||
|
<div className="mx_Spinner">
|
||||||
|
<div className="mx_Spinner_Msg">{msg}</div>
|
||||||
|
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
212
src/components/views/elements/Pill.js
Normal file
212
src/components/views/elements/Pill.js
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import 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. This excludes event URLs (with `[^\/]*`)
|
||||||
|
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||||
|
|
||||||
|
const Pill = React.createClass({
|
||||||
|
statics: {
|
||||||
|
isPillUrl: (url) => {
|
||||||
|
return !!REGEX_MATRIXTO.exec(url);
|
||||||
|
},
|
||||||
|
isMessagePillUrl: (url) => {
|
||||||
|
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||||
|
},
|
||||||
|
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||||
|
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||||
|
},
|
||||||
|
|
||||||
|
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),
|
||||||
|
// Whether to include an avatar in the pill
|
||||||
|
shouldShowPillAvatar: PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState() {
|
||||||
|
return {
|
||||||
|
// ID/alias of the room/user
|
||||||
|
resourceId: null,
|
||||||
|
// Type of pill
|
||||||
|
pillType: null,
|
||||||
|
|
||||||
|
// The member related to the user pill
|
||||||
|
member: null,
|
||||||
|
// The room related to the room pill
|
||||||
|
room: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
let regex = REGEX_MATRIXTO;
|
||||||
|
if (nextProps.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(nextProps.url) || [];
|
||||||
|
|
||||||
|
const resourceId = matrixToMatch[1]; // The room/user ID
|
||||||
|
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||||
|
|
||||||
|
const pillType = {
|
||||||
|
'@': Pill.TYPE_USER_MENTION,
|
||||||
|
'#': Pill.TYPE_ROOM_MENTION,
|
||||||
|
'!': Pill.TYPE_ROOM_MENTION,
|
||||||
|
}[prefix];
|
||||||
|
|
||||||
|
let member;
|
||||||
|
let room;
|
||||||
|
switch (pillType) {
|
||||||
|
case Pill.TYPE_USER_MENTION: {
|
||||||
|
const localMember = nextProps.room.getMember(resourceId);
|
||||||
|
member = localMember;
|
||||||
|
if (!localMember) {
|
||||||
|
member = new RoomMember(null, resourceId);
|
||||||
|
this.doProfileLookup(resourceId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Pill.TYPE_ROOM_MENTION: {
|
||||||
|
const localRoom = resourceId[0] === '#' ?
|
||||||
|
MatrixClientPeg.get().getRooms().find((r) => {
|
||||||
|
return r.getAliases().includes(resourceId);
|
||||||
|
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||||
|
room = localRoom;
|
||||||
|
if (!localRoom) {
|
||||||
|
// TODO: This would require a new API to resolve a room alias to
|
||||||
|
// a room avatar and name.
|
||||||
|
// this.doRoomProfileLookup(resourceId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setState({resourceId, pillType, member, room});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this.componentWillReceiveProps(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
doProfileLookup: function(userId, member) {
|
||||||
|
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
member.name = resp.displayname;
|
||||||
|
member.rawDisplayName = resp.displayname;
|
||||||
|
member.events.member = {
|
||||||
|
getContent: () => {
|
||||||
|
return {avatar_url: resp.avatar_url};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.setState({member});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Could not retrieve profile data for ' + userId + ':', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUserPillClicked: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_user',
|
||||||
|
member: this.state.member,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
|
||||||
|
const resource = this.state.resourceId;
|
||||||
|
|
||||||
|
let avatar = null;
|
||||||
|
let linkText = resource;
|
||||||
|
let pillClass;
|
||||||
|
let userId;
|
||||||
|
let href = this.props.url;
|
||||||
|
let onClick;
|
||||||
|
switch (this.state.pillType) {
|
||||||
|
case Pill.TYPE_USER_MENTION: {
|
||||||
|
// If this user is not a member of this room, default to the empty member
|
||||||
|
const member = this.state.member;
|
||||||
|
if (member) {
|
||||||
|
userId = member.userId;
|
||||||
|
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
|
if (this.props.shouldShowPillAvatar) {
|
||||||
|
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||||
|
}
|
||||||
|
pillClass = 'mx_UserPill';
|
||||||
|
href = null;
|
||||||
|
onClick = this.onUserPillClicked.bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Pill.TYPE_ROOM_MENTION: {
|
||||||
|
const room = this.state.room;
|
||||||
|
if (room) {
|
||||||
|
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||||
|
if (this.props.shouldShowPillAvatar) {
|
||||||
|
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||||
|
}
|
||||||
|
pillClass = 'mx_RoomPill';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames(pillClass, {
|
||||||
|
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.pillType) {
|
||||||
|
return this.props.inMessage ?
|
||||||
|
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
|
{avatar}
|
||||||
|
{linkText}
|
||||||
|
</a> :
|
||||||
|
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
|
{avatar}
|
||||||
|
{linkText}
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
// Deliberately render nothing if the URL isn't recognised
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Pill;
|
|
@ -69,12 +69,21 @@ class PasswordLogin extends React.Component {
|
||||||
|
|
||||||
onSubmitForm(ev) {
|
onSubmitForm(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||||
this.props.onSubmit(
|
this.props.onSubmit(
|
||||||
this.state.username,
|
'', // XXX: Synapse breaks if you send null here:
|
||||||
this.state.phoneCountry,
|
this.state.phoneCountry,
|
||||||
this.state.phoneNumber,
|
this.state.phoneNumber,
|
||||||
this.state.password,
|
this.state.password,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onSubmit(
|
||||||
|
this.state.username,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this.state.password,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUsernameChanged(ev) {
|
onUsernameChanged(ev) {
|
||||||
|
|
|
@ -95,7 +95,7 @@ module.exports = React.createClass({
|
||||||
if (this.allFieldsValid()) {
|
if (this.allFieldsValid()) {
|
||||||
if (this.refs.email.value == '') {
|
if (this.refs.email.value == '') {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -122,7 +122,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
showHelpPopup: function() {
|
showHelpPopup: function() {
|
||||||
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
||||||
Modal.createDialog(CustomServerDialog);
|
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
|
@ -282,7 +282,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err);
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Error decrypting attachment"),
|
description: _t("Error decrypting attachment"),
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,7 @@ import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
||||||
this.fixupHeight();
|
this.fixupHeight();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
let thumbnailPromise = q(null);
|
let thumbnailPromise = Promise.resolve(null);
|
||||||
if (content.info.thumbnail_file) {
|
if (content.info.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file,
|
content.info.thumbnail_file,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
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);
|
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
var thumbnailPromise = q(null);
|
var thumbnailPromise = Promise.resolve(null);
|
||||||
if (content.info.thumbnail_file) {
|
if (content.info.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file
|
content.info.thumbnail_file
|
||||||
|
|
|
@ -29,6 +29,10 @@ import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import UserSettingsStore from "../../../UserSettingsStore";
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import {RoomMember} from 'matrix-js-sdk';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -79,6 +83,10 @@ module.exports = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this._unmounted = false;
|
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);
|
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||||
this.calculateUrlPreview();
|
this.calculateUrlPreview();
|
||||||
|
|
||||||
|
@ -90,8 +98,19 @@ module.exports = React.createClass({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; 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]);
|
highlight.highlightBlock(blocks[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
// add event handlers to the 'copy code' buttons
|
// add event handlers to the 'copy code' buttons
|
||||||
|
@ -131,9 +150,15 @@ module.exports = React.createClass({
|
||||||
if (this.props.showUrlPreview && !this.state.links.length) {
|
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||||
var links = this.findLinks(this.refs.content.children);
|
var links = this.findLinks(this.refs.content.children);
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
this.setState({ links: links.map((link)=>{
|
// de-dup the links (but preserve ordering)
|
||||||
return link.getAttribute("href");
|
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
|
// lazy-load the hidden state of the preview widget from localstorage
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
|
@ -144,14 +169,44 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pillifyLinks: function(nodes) {
|
||||||
|
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||||
|
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}
|
||||||
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
ReactDOM.render(pill, pillContainer);
|
||||||
|
node.parentNode.replaceChild(pillContainer, node);
|
||||||
|
}
|
||||||
|
} else if (node.children && node.children.length) {
|
||||||
|
this.pillifyLinks(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
findLinks: function(nodes) {
|
findLinks: function(nodes) {
|
||||||
var links = [];
|
var links = [];
|
||||||
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
var node = nodes[i];
|
var node = nodes[i];
|
||||||
if (node.tagName === "A" && node.getAttribute("href"))
|
if (node.tagName === "A" && node.getAttribute("href"))
|
||||||
{
|
{
|
||||||
if (this.isLinkPreviewable(node)) {
|
if (this.isLinkPreviewable(node)) {
|
||||||
links.push(node);
|
links.push(node.getAttribute("href"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||||
|
@ -213,26 +268,28 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onEmoteSenderClick: function(event) {
|
onEmoteSenderClick: function(event) {
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'insert_displayname',
|
action: 'insert_mention',
|
||||||
displayname: name.replace(' (IRC)', ''),
|
user_id: mxEvent.getSender(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventTileOps: function() {
|
getEventTileOps: function() {
|
||||||
var self = this;
|
|
||||||
return {
|
return {
|
||||||
isWidgetHidden: function() {
|
isWidgetHidden: () => {
|
||||||
return self.state.widgetHidden;
|
return this.state.widgetHidden;
|
||||||
},
|
},
|
||||||
|
|
||||||
unhideWidget: function() {
|
unhideWidget: () => {
|
||||||
self.setState({ widgetHidden: false });
|
this.setState({ widgetHidden: false });
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
|
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInnerText: () => {
|
||||||
|
return this.refs.content.innerText;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -251,7 +308,7 @@ module.exports = React.createClass({
|
||||||
let completeUrl = scalarClient.getStarterLink(starterLink);
|
let completeUrl = scalarClient.getStarterLink(starterLink);
|
||||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
|
||||||
title: _t("Add an Integration"),
|
title: _t("Add an Integration"),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ObjectUtils = require("../../../ObjectUtils");
|
var ObjectUtils = require("../../../ObjectUtils");
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||||
console.log("AliasSettings: Updating canonical alias");
|
console.log("AliasSettings: Updating canonical alias");
|
||||||
promises = [q.all(promises).then(
|
promises = [Promise.all(promises).then(
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
this.props.roomId, "m.room.canonical_alias", {
|
this.props.roomId, "m.room.canonical_alias", {
|
||||||
alias: this.state.canonicalAlias
|
alias: this.state.canonicalAlias
|
||||||
|
@ -154,7 +154,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||||
title: _t('Invalid alias format'),
|
title: _t('Invalid alias format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||||
});
|
});
|
||||||
|
@ -170,7 +170,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||||
title: _t('Invalid address format'),
|
title: _t('Invalid address format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
@ -72,7 +72,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
saveSettings: function() { // : Promise
|
saveSettings: function() { // : Promise
|
||||||
if (!this.state.hasChanged) {
|
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();
|
var originalState = this.getInitialState();
|
||||||
if (originalState.primary_color !== this.state.primary_color ||
|
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) {
|
_getColorIndex: function(scheme) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
var sdk = require("../../../index");
|
var sdk = require("../../../index");
|
||||||
|
|
207
src/components/views/rooms/AppsDrawer.js
Normal file
207
src/components/views/rooms/AppsDrawer.js
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
/*
|
||||||
|
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';
|
||||||
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_canUserModify: function() {
|
||||||
|
try {
|
||||||
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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.createTrackedDialog('Integrations Manager', '', 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 && this._canUserModify() &&
|
||||||
|
(<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 isEqual from 'lodash/isEqual';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||||
import Q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
|
@ -39,26 +40,62 @@ export default class Autocomplete extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillReceiveProps(props, state) {
|
componentWillReceiveProps(newProps, state) {
|
||||||
if (props.query === this.props.query) {
|
// Query hasn't changed so don't try to complete it
|
||||||
return null;
|
if (newProps.query === this.props.query) {
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return;
|
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.
|
// Reset selection when completion list becomes empty.
|
||||||
let selectionOffset = COMPOSER_SELECTED;
|
let selectionOffset = COMPOSER_SELECTED;
|
||||||
|
@ -69,33 +106,26 @@ export default class Autocomplete extends React.Component {
|
||||||
const currentSelection = this.state.selectionOffset === 0 ? null :
|
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||||
selectionOffset = completionList.findIndex(
|
selectionOffset = completionList.findIndex(
|
||||||
completion => completion.completion === currentSelection);
|
(completion) => completion.completion === currentSelection);
|
||||||
if (selectionOffset === -1) {
|
if (selectionOffset === -1) {
|
||||||
selectionOffset = COMPOSER_SELECTED;
|
selectionOffset = COMPOSER_SELECTED;
|
||||||
} else {
|
} else {
|
||||||
selectionOffset++; // selectionOffset is 1-indexed!
|
selectionOffset++; // selectionOffset is 1-indexed!
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If no completions were returned, we should turn off force completion.
|
|
||||||
forceComplete = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hide = this.state.hide;
|
let hide = this.state.hide;
|
||||||
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
// If `completion.command.command` is truthy, then a provider has matched with the query
|
||||||
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
|
const anyMatches = completions.some((completion) => !!completion.command.command);
|
||||||
newMatches = completions.map(completion => !!completion.command.command);
|
hide = !anyMatches;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
completions,
|
completions,
|
||||||
completionList,
|
completionList,
|
||||||
selectionOffset,
|
selectionOffset,
|
||||||
hide,
|
hide,
|
||||||
forceComplete,
|
// Force complete is turned off each time since we can't edit the query in that case
|
||||||
|
forceComplete: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,16 +172,17 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.setState({hide: true, selectionOffset: 0});
|
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
forceComplete() {
|
forceComplete() {
|
||||||
const done = Q.defer();
|
const done = Promise.defer();
|
||||||
this.setState({
|
this.setState({
|
||||||
forceComplete: true,
|
forceComplete: true,
|
||||||
|
hide: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
this.complete(this.props.query, this.props.selection).then(() => {
|
this.complete(this.props.query, this.props.selection).then(() => {
|
||||||
done.resolve();
|
done.resolve(this.countCompletions());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return done.promise;
|
return done.promise;
|
||||||
|
@ -169,7 +200,7 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelection(selectionOffset: number) {
|
setSelection(selectionOffset: number) {
|
||||||
this.setState({selectionOffset});
|
this.setState({selectionOffset, hide: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -185,21 +216,24 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(state, func) {
|
||||||
|
super.setState(state, func);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let position = 1;
|
let position = 1;
|
||||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
const completions = completionResult.completions.map((completion, i) => {
|
||||||
|
|
||||||
const className = classNames('mx_Autocomplete_Completion', {
|
const className = classNames('mx_Autocomplete_Completion', {
|
||||||
'selected': position === this.state.selectionOffset,
|
'selected': position === this.state.selectionOffset,
|
||||||
});
|
});
|
||||||
let componentPosition = position;
|
const componentPosition = position;
|
||||||
position++;
|
position++;
|
||||||
|
|
||||||
let onMouseOver = () => this.setSelection(componentPosition);
|
const onMouseOver = () => this.setSelection(componentPosition);
|
||||||
let onClick = () => {
|
const onClick = () => {
|
||||||
this.setSelection(componentPosition);
|
this.setSelection(componentPosition);
|
||||||
this.onCompletionClicked();
|
this.onCompletionClicked();
|
||||||
};
|
};
|
||||||
|
@ -220,7 +254,7 @@ export default class Autocomplete extends React.Component {
|
||||||
{completionResult.provider.renderCompletions(completions)}
|
{completionResult.provider.renderCompletions(completions)}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}).filter(completion => !!completion);
|
}).filter((completion) => !!completion);
|
||||||
|
|
||||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||||
|
|
|
@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import ObjectUtils from '../../../ObjectUtils';
|
import ObjectUtils from '../../../ObjectUtils';
|
||||||
|
import AppsDrawer from './AppsDrawer';
|
||||||
import { _t, _tJsx} from '../../../languageHandler';
|
import { _t, _tJsx} from '../../../languageHandler';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -28,6 +30,8 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
showApps: React.PropTypes.bool,
|
||||||
|
|
||||||
// Conference Handler implementation
|
// Conference Handler implementation
|
||||||
conferenceHandler: React.PropTypes.object,
|
conferenceHandler: React.PropTypes.object,
|
||||||
|
@ -70,10 +74,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var CallView = sdk.getComponent("voip.CallView");
|
const CallView = sdk.getComponent("voip.CallView");
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
var fileDropTarget = null;
|
let fileDropTarget = null;
|
||||||
if (this.props.draggingFile) {
|
if (this.props.draggingFile) {
|
||||||
fileDropTarget = (
|
fileDropTarget = (
|
||||||
<div className="mx_RoomView_fileDropTarget">
|
<div className="mx_RoomView_fileDropTarget">
|
||||||
|
@ -87,14 +91,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var conferenceCallNotification = null;
|
let conferenceCallNotification = null;
|
||||||
if (this.props.displayConfCallNotification) {
|
if (this.props.displayConfCallNotification) {
|
||||||
let supportedText = '';
|
let supportedText = '';
|
||||||
let joinNode;
|
let joinNode;
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
supportedText = _t(" (unsupported)");
|
supportedText = _t(" (unsupported)");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
joinNode = (<span>
|
joinNode = (<span>
|
||||||
{_tJsx(
|
{_tJsx(
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||||
|
@ -105,7 +108,6 @@ module.exports = React.createClass({
|
||||||
]
|
]
|
||||||
)}
|
)}
|
||||||
</span>);
|
</span>);
|
||||||
|
|
||||||
}
|
}
|
||||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
// 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.
|
// 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}
|
<CallView ref="callView" room={this.props.room}
|
||||||
ConferenceHandler={this.props.conferenceHandler}
|
ConferenceHandler={this.props.conferenceHandler}
|
||||||
onResize={this.props.onResize}
|
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 (
|
return (
|
||||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||||
|
{ appsDrawer }
|
||||||
{ fileDropTarget }
|
{ fileDropTarget }
|
||||||
{ callView }
|
{ callView }
|
||||||
{ conferenceCallNotification }
|
{ conferenceCallNotification }
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue