Merge branch 'develop' into erikj/group_server
This commit is contained in:
commit
32a01b54b8
61 changed files with 2577 additions and 1482 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -116,11 +116,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
|
||||||
|
|
12
package.json
12
package.json
|
@ -33,15 +33,16 @@
|
||||||
"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 $KARMAFLAGS --single-run=true --browsers ChromeHeadless",
|
||||||
"test-multi": "karma start $KARMAFLAGS --single-run=false"
|
"test-multi": "karma start $KARMAFLAGS"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
"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.11.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"
|
||||||
},
|
},
|
||||||
|
@ -105,12 +107,10 @@
|
||||||
"karma-cli": "^0.1.2",
|
"karma-cli": "^0.1.2",
|
||||||
"karma-junit-reporter": "^0.4.1",
|
"karma-junit-reporter": "^0.4.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-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
"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,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;
|
||||||
|
|
|
@ -52,21 +52,19 @@ export default class ComposerHistoryManager {
|
||||||
history: Array<HistoryItem> = [];
|
history: Array<HistoryItem> = [];
|
||||||
prefix: string;
|
prefix: string;
|
||||||
lastIndex: number = 0;
|
lastIndex: number = 0;
|
||||||
currentIndex: number = -1;
|
currentIndex: number = 0;
|
||||||
|
|
||||||
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||||
this.prefix = prefix + roomId;
|
this.prefix = prefix + roomId;
|
||||||
|
|
||||||
// TODO: Performance issues?
|
// TODO: Performance issues?
|
||||||
for(; sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`); this.lastIndex++, this.currentIndex++) {
|
let item;
|
||||||
|
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||||
this.history.push(
|
this.history.push(
|
||||||
Object.assign(
|
Object.assign(new HistoryItem(), JSON.parse(item)),
|
||||||
new HistoryItem(),
|
|
||||||
JSON.parse(sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`)),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.currentIndex--;
|
this.lastIndex = this.currentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(message: string, format: MessageFormat) {
|
addItem(message: string, format: MessageFormat) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -84,7 +84,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;
|
||||||
|
|
||||||
|
@ -93,10 +93,21 @@ export function stripParagraphs(html: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentHTML = "";
|
let contentHTML = "";
|
||||||
for (let i=0; i<contentDiv.children.length; i++) {
|
for (let i=0; i < contentDiv.children.length; i++) {
|
||||||
const element = contentDiv.children[i];
|
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));
|
||||||
|
@ -134,6 +145,7 @@ const sanitizeHtmlParams = {
|
||||||
// would make sense if we did
|
// would make sense if we did
|
||||||
img: ['src'],
|
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'],
|
||||||
|
@ -175,6 +187,19 @@ const 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 };
|
||||||
},
|
},
|
||||||
|
'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
|
||||||
|
|
|
@ -30,7 +30,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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -419,6 +419,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
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -77,22 +77,26 @@ class MatrixClientPeg {
|
||||||
this._createClient(creds);
|
this._createClient(creds);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
async start() {
|
||||||
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";
|
||||||
|
|
||||||
let promise = this.matrixClient.store.startup();
|
try {
|
||||||
// log any errors when starting up the database (if one exists)
|
let promise = this.matrixClient.store.startup();
|
||||||
promise.catch((err) => {
|
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
||||||
|
await promise;
|
||||||
|
} catch(err) {
|
||||||
|
// log any errors when starting up the database (if one exists)
|
||||||
console.error(`Error starting matrixclient store: ${err}`);
|
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(() => {
|
|
||||||
this.get().startClient(opts);
|
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||||
});
|
this.get().startClient(opts);
|
||||||
|
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(): MatrixClientCreds {
|
getCredentials(): MatrixClientCreds {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function levelRoleMap() {
|
||||||
return {
|
return {
|
||||||
undefined: _t('Default'),
|
undefined: _t('Default'),
|
||||||
0: _t('User'),
|
0: _t('User'),
|
||||||
50: _t('Moderator'),
|
50: _t('Moderator'),
|
||||||
100: _t('Admin'),
|
100: _t('Admin'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,11 +80,11 @@ 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 q.all(promises);
|
||||||
|
@ -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,8 +119,8 @@ 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));
|
||||||
|
@ -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'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -109,6 +110,76 @@ Example:
|
||||||
response: 78
|
response: 78
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 the `content` field
|
||||||
|
of the state event.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- `room_id` (String) is the room to get the widgets in.
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
$widget_id: {
|
||||||
|
type: "example",
|
||||||
|
url: "http://widget.url",
|
||||||
|
name: "Example Widget",
|
||||||
|
data: {
|
||||||
|
key: "val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$widget_id: { ... }
|
||||||
|
}
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "get_widgets",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
widget_id: "abc123",
|
||||||
|
url: "http://widget.url",
|
||||||
|
type: "example",
|
||||||
|
response: {
|
||||||
|
$widget_id: {
|
||||||
|
type: "example",
|
||||||
|
url: "http://widget.url",
|
||||||
|
name: "Example Widget",
|
||||||
|
data: {
|
||||||
|
key: "val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$widget_id: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
membership_state AND bot_options
|
membership_state AND bot_options
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
@ -191,6 +262,84 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this.
|
||||||
|
client.getStateEvent(roomId, "im.vector.modular.widgets", "").then((widgets) => {
|
||||||
|
if (widgetUrl === null) {
|
||||||
|
delete widgets[widgetId];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
widgets[widgetId] = {
|
||||||
|
type: widgetType,
|
||||||
|
url: widgetUrl,
|
||||||
|
name: widgetName,
|
||||||
|
data: widgetData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets);
|
||||||
|
}, (err) => {
|
||||||
|
if (err.errcode === "M_NOT_FOUND") {
|
||||||
|
return client.sendStateEvent(roomId, "im.vector.modular.widgets", {
|
||||||
|
[widgetId]: {
|
||||||
|
type: widgetType,
|
||||||
|
url: widgetUrl,
|
||||||
|
name: widgetName,
|
||||||
|
data: widgetData,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}).done(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
sendError(event, _t('Failed to send request.'), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWidgets(event, roomId) {
|
||||||
|
returnStateEvent(event, roomId, "im.vector.modular.widgets", "");
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
@ -367,7 +516,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 +526,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
@ -409,12 +564,27 @@ const onMessage = function(event) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let listenerCount = 0;
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startListening: function() {
|
startListening: function() {
|
||||||
window.addEventListener("message", onMessage, false);
|
if (listenerCount === 0) {
|
||||||
|
window.addEventListener("message", onMessage, false);
|
||||||
|
}
|
||||||
|
listenerCount += 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
stopListening: function() {
|
stopListening: function() {
|
||||||
window.removeEventListener("message", onMessage);
|
listenerCount -= 1;
|
||||||
|
if (listenerCount === 0) {
|
||||||
|
window.removeEventListener("message", onMessage);
|
||||||
|
}
|
||||||
|
if (listenerCount < 0) {
|
||||||
|
// Make an error so we get a stack trace
|
||||||
|
const e = new Error(
|
||||||
|
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
||||||
|
" Negative count"
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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;
|
||||||
|
|
|
@ -186,7 +186,7 @@ const commands = {
|
||||||
if (targetRoomId) { break; }
|
if (targetRoomId) { break; }
|
||||||
}
|
}
|
||||||
if (!targetRoomId) {
|
if (!targetRoomId) {
|
||||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,8 +344,7 @@ const commands = {
|
||||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
_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' +
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
||||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
|
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,11 +30,17 @@ export default {
|
||||||
id: 'rich_text_editor',
|
id: 'rich_text_editor',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "-",
|
||||||
|
id: 'matrix_apps',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// 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("New Composer & Autocomplete");
|
||||||
|
this.LABS_FEATURES[1].name = _t("Matrix Apps");
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
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 = [
|
||||||
{
|
{
|
||||||
|
@ -28,11 +29,6 @@ const COMMANDS = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: 'Displays action',
|
description: 'Displays action',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
command: '/part',
|
|
||||||
args: '[#alias:domain]',
|
|
||||||
description: 'Leave room',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
command: '/ban',
|
command: '/ban',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
|
@ -43,6 +39,11 @@ const COMMANDS = [
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Unbans user with given 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>',
|
||||||
|
@ -58,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]',
|
||||||
|
@ -74,10 +85,16 @@ const COMMANDS = [
|
||||||
description: 'Searches DuckDuckGo for results',
|
description: 'Searches DuckDuckGo for results',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/op',
|
command: '/tint',
|
||||||
args: '<userId> [<power level>]',
|
args: '<color1> [<color2>]',
|
||||||
description: 'Define the power level of a user',
|
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;
|
||||||
|
|
|
@ -18,16 +18,42 @@ 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} from 'emojione';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
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';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
import EmojiData from '../stripped-emoji.json';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList).map(shortname => {
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
const CATEGORY_ORDER = [
|
||||||
|
'people',
|
||||||
|
'food',
|
||||||
|
'objects',
|
||||||
|
'activity',
|
||||||
|
'nature',
|
||||||
|
'travel',
|
||||||
|
'flags',
|
||||||
|
'symbols',
|
||||||
|
'unicode9',
|
||||||
|
'modifier',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match for ":wink:" or ascii-style ";-)" provided by emojione
|
||||||
|
const EMOJI_REGEX = new RegExp('(' + asciiRegexp + '|:\\w*:?)$', 'g');
|
||||||
|
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) => {
|
||||||
return {
|
return {
|
||||||
shortname,
|
name: a.name,
|
||||||
|
shortname: a.shortname,
|
||||||
|
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +63,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
|
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
|
||||||
keys: 'shortname',
|
keys: ['aliases_ascii', 'shortname', 'name'],
|
||||||
|
// For matching against ascii equivalents
|
||||||
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 8);
|
}).slice(0, LIMIT);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
@ -73,7 +101,7 @@ export default class EmojiProvider 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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,18 @@ export default class QueryMatcher {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.keys = options.keys;
|
this.keys = options.keys;
|
||||||
this.setObjects(objects);
|
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>) {
|
setObjects(objects: Array<Object>) {
|
||||||
|
@ -70,10 +82,31 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
match(query: String): Array<Object> {
|
match(query: String): Array<Object> {
|
||||||
query = query.toLowerCase().replace(/[^\w]/g, '');
|
query = query.toLowerCase();
|
||||||
const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => {
|
if (this.options.shouldMatchWordsOnly) {
|
||||||
return key.toLowerCase().replace(/[^\w]/g, '').indexOf(query) >= 0 ? this.keyMap.objectMap[key] : [];
|
query = query.replace(/[^\w]/g, '');
|
||||||
}), (candidate) => this.keyMap.priorityMap.get(candidate)));
|
}
|
||||||
return results;
|
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 _sortedUniq(_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];
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ 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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,10 +37,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name'],
|
||||||
});
|
});
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name'],
|
||||||
|
shouldMatchPrefix: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.matcher.match(command[0]).map(user => {
|
completions = this.matcher.match(command[0]).slice(0, 4).map((user) => {
|
||||||
let 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;
|
let completion = displayName;
|
||||||
if (range.start === 0) {
|
if (range.start === 0) {
|
||||||
|
@ -68,7 +69,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
});
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +91,9 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
if (member.userId !== currentUserId) return true;
|
if (member.userId !== currentUserId) return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.users = _sortBy(this.users, (user) => 1E20 - lastSpoken[user.userId] || 1E20);
|
this.users = _sortBy(this.users, (completion) =>
|
||||||
|
1E20 - lastSpoken[completion.user.userId] || 1E20,
|
||||||
|
);
|
||||||
|
|
||||||
this.matcher.setObjects(this.users);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
@ -98,9 +101,10 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
onUserSpoke(user: RoomMember) {
|
onUserSpoke(user: RoomMember) {
|
||||||
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
// Probably unsafe to compare by reference here?
|
this.users = this.users.splice(
|
||||||
_pull(this.users, user);
|
this.users.findIndex((user2) => user2.userId === user.userId), 1);
|
||||||
this.users.splice(0, 0, user);
|
this.users = [user, ...this.users];
|
||||||
|
|
||||||
this.matcher.setObjects(this.users);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +116,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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,13 +47,12 @@ import UserProvider from '../../autocomplete/UserProvider';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
|
|
||||||
var DEBUG = false;
|
let DEBUG = false;
|
||||||
|
let debuglog = function() {};
|
||||||
|
|
||||||
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 +112,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,
|
||||||
|
@ -236,6 +236,7 @@ module.exports = React.createClass({
|
||||||
if (room) {
|
if (room) {
|
||||||
this.setState({
|
this.setState({
|
||||||
unsentMessageError: this._getUnsentMessageError(room),
|
unsentMessageError: this._getUnsentMessageError(room),
|
||||||
|
showApps: this._shouldShowApps(room),
|
||||||
});
|
});
|
||||||
this._onRoomLoaded(room);
|
this._onRoomLoaded(room);
|
||||||
}
|
}
|
||||||
|
@ -273,6 +274,11 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_shouldShowApps: function(room) {
|
||||||
|
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
return appsStateEvents && Object.keys(appsStateEvents.getContent()).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
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";
|
||||||
|
@ -453,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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1604,11 +1615,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>
|
||||||
);
|
);
|
||||||
|
@ -1621,8 +1634,14 @@ 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}
|
||||||
|
tabComplete={this.tabComplete}
|
||||||
|
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
|
||||||
|
|
|
@ -93,6 +93,10 @@ const SETTINGS_LABELS = [
|
||||||
id: 'disableMarkdown',
|
id: 'disableMarkdown',
|
||||||
label: 'Disable markdown formatting',
|
label: 'Disable markdown formatting',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'enableSyntaxHighlightLanguageDetection',
|
||||||
|
label: 'Enable automatic language detection for syntax highlighting',
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
id: 'useFixedWidthFont',
|
id: 'useFixedWidthFont',
|
||||||
|
|
161
src/components/views/elements/AppTile.js
Normal file
161
src/components/views/elements/AppTile.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
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 ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
widgetUrl: this.props.url,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||||
|
isScalarUrl: function() {
|
||||||
|
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
||||||
|
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
if (!this.isScalarUrl()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fetch the token before loading the iframe as we need to mangle the URL
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
this._scalarClient = new ScalarAuthClient();
|
||||||
|
this._scalarClient.getScalarToken().done((token) => {
|
||||||
|
// Append scalar_token as a query param
|
||||||
|
const u = url.parse(this.props.url);
|
||||||
|
if (!u.search) {
|
||||||
|
u.search = "?scalar_token=" + encodeURIComponent(token);
|
||||||
|
} else {
|
||||||
|
u.search += "&scalar_token=" + encodeURIComponent(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
error: null,
|
||||||
|
widgetUrl: u.format(),
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
this.setState({
|
||||||
|
error: err.message,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEditClick: function() {
|
||||||
|
console.log("Edit widget %s", this.props.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDeleteClick: function() {
|
||||||
|
console.log("Delete widget %s", this.props.id);
|
||||||
|
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
if (!appsStateEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const appsStateEvent = appsStateEvents.getContent();
|
||||||
|
if (appsStateEvent[this.props.id]) {
|
||||||
|
delete appsStateEvent[this.props.id];
|
||||||
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
|
this.props.room.roomId,
|
||||||
|
'im.vector.modular.widgets',
|
||||||
|
appsStateEvent,
|
||||||
|
'',
|
||||||
|
).then(() => {
|
||||||
|
console.log('Deleted widget');
|
||||||
|
}, (e) => {
|
||||||
|
console.error('Failed to delete widget', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (this.state.loading) {
|
||||||
|
appTileBody = (
|
||||||
|
<div> Loading... </div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<iframe ref="appFrame" src={this.state.widgetUrl} allowFullScreen="true"></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
|
<div className="mx_AppTileMenuBar">
|
||||||
|
{this.formatAppTileName()}
|
||||||
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
|
{/* Edit widget */}
|
||||||
|
{/* <img
|
||||||
|
src="img/edit.svg"
|
||||||
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
width="8" height="8" alt="Edit"
|
||||||
|
onClick={this._onEditClick}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* Delete widget */}
|
||||||
|
<img src="img/cancel.svg"
|
||||||
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
||||||
|
width="8" height="8" alt={_t("Cancel")}
|
||||||
|
onClick={this._onDeleteClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{appTileBody}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -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;
|
||||||
|
|
|
@ -29,6 +29,7 @@ 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";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -90,7 +91,18 @@ 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++) {
|
||||||
highlight.highlightBlock(blocks[i]);
|
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||||
|
highlight.highlightBlock(blocks[i])
|
||||||
|
} else {
|
||||||
|
// Only syntax highlight if there's a class starting with language-
|
||||||
|
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
|
||||||
|
return cl.startsWith('language-');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (classes.length != 0) {
|
||||||
|
highlight.highlightBlock(blocks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
@ -131,9 +143,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) {
|
||||||
|
@ -146,12 +164,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
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" ||
|
||||||
|
|
218
src/components/views/rooms/AppsDrawer.js
Normal file
218
src/components/views/rooms/AppsDrawer.js
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import AppTile from '../elements/AppTile';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'AppsDrawer',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
apps: this._getApps(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.scalarClient = null;
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
|
this.scalarClient = new ScalarAuthClient();
|
||||||
|
this.scalarClient.connect().done(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
if (this.state.apps && this.state.apps.length < 1) {
|
||||||
|
this.onClickAddWidget();
|
||||||
|
}
|
||||||
|
// TODO -- Handle Scalar errors
|
||||||
|
// },
|
||||||
|
// (err) => {
|
||||||
|
// this.setState({
|
||||||
|
// scalar_error: err,
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
ScalarMessaging.stopListening();
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a URI according to a set of template variables. Variables will be
|
||||||
|
* passed through encodeURIComponent.
|
||||||
|
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||||
|
* @param {Object} variables The key/value pairs to replace the template
|
||||||
|
* variables with. E.g. { "$bar": "baz" }.
|
||||||
|
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||||
|
*/
|
||||||
|
encodeUri: function(pathTemplate, variables) {
|
||||||
|
for (const key in variables) {
|
||||||
|
if (!variables.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pathTemplate = pathTemplate.replace(
|
||||||
|
key, encodeURIComponent(variables[key]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return pathTemplate;
|
||||||
|
},
|
||||||
|
|
||||||
|
_initAppConfig: function(appId, app) {
|
||||||
|
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||||
|
const params = {
|
||||||
|
'$matrix_user_id': this.props.userId,
|
||||||
|
'$matrix_room_id': this.props.room.roomId,
|
||||||
|
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||||
|
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if(app.data) {
|
||||||
|
Object.keys(app.data).forEach((key) => {
|
||||||
|
params['$' + key] = app.data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.id = appId;
|
||||||
|
app.name = app.name || app.type;
|
||||||
|
app.url = this.encodeUri(app.url, params);
|
||||||
|
|
||||||
|
// switch(app.type) {
|
||||||
|
// case 'etherpad':
|
||||||
|
// app.queryParams = '?userName=' + this.props.userId +
|
||||||
|
// '&padId=' + this.props.room.roomId;
|
||||||
|
// break;
|
||||||
|
// case 'jitsi': {
|
||||||
|
//
|
||||||
|
// app.queryParams = '?confId=' + app.data.confId +
|
||||||
|
// '&displayName=' + encodeURIComponent(user.displayName) +
|
||||||
|
// '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) +
|
||||||
|
// '&email=' + encodeURIComponent(this.props.userId) +
|
||||||
|
// '&isAudioConf=' + app.data.isAudioConf;
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case 'vrdemo':
|
||||||
|
// app.queryParams = '?roomAlias=' + encodeURIComponent(app.data.roomAlias);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
const appsStateEvent = appsStateEvents.getContent();
|
||||||
|
if (Object.keys(appsStateEvent).length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(appsStateEvent).map((appId) => {
|
||||||
|
return this._initAppConfig(appId, appsStateEvent[appId]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateApps: function() {
|
||||||
|
const apps = this._getApps();
|
||||||
|
if (apps.length < 1) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
apps: apps,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickAddWidget: function(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||||
|
null;
|
||||||
|
Modal.createDialog(IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const apps = this.state.apps.map(
|
||||||
|
(app, index, arr) => {
|
||||||
|
return <AppTile
|
||||||
|
key={app.name}
|
||||||
|
id={app.id}
|
||||||
|
url={app.url}
|
||||||
|
name={app.name}
|
||||||
|
fullWidth={arr.length<2 ? true : false}
|
||||||
|
room={this.props.room}
|
||||||
|
userId={this.props.userId}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addWidget = this.state.apps && this.state.apps.length < 2 &&
|
||||||
|
(<div onClick={this.onClickAddWidget}
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
className="mx_AddWidget_button"
|
||||||
|
title={_t('Add a widget')}>
|
||||||
|
[+] {_t('Add a widget')}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_AppsDrawer">
|
||||||
|
<div id="apps" className="mx_AppsContainer">
|
||||||
|
{apps}
|
||||||
|
</div>
|
||||||
|
{addWidget}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -40,25 +40,51 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.complete(newProps.query, newProps.selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(query, selection) {
|
||||||
|
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 Q(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 = Q.defer();
|
||||||
|
this.debounceCompletionsRequest = setTimeout(() => {
|
||||||
|
getCompletions(
|
||||||
|
query, selection, this.state.forceComplete,
|
||||||
|
).then((completions) => {
|
||||||
|
this.processCompletions(completions);
|
||||||
|
deferred.resolve();
|
||||||
|
});
|
||||||
|
}, autocompleteDelay);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
processCompletions(completions) {
|
||||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||||
|
|
||||||
// Reset selection when completion list becomes empty.
|
// Reset selection when completion list becomes empty.
|
||||||
|
@ -88,23 +114,13 @@ export default class Autocomplete extends React.Component {
|
||||||
hide = false;
|
hide = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const autocompleteDelay = UserSettingsStore.getSyncedSetting('autocompleteDelay', 200);
|
|
||||||
|
|
||||||
// We had no completions before, but do now, so we should apply our display delay here
|
|
||||||
if (this.state.completionList.length === 0 && completionList.length > 0 &&
|
|
||||||
!forceComplete && autocompleteDelay > 0) {
|
|
||||||
await Q.delay(autocompleteDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force complete is turned off each time since we can't edit the query in that case
|
|
||||||
forceComplete = 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +177,7 @@ export default class Autocomplete extends React.Component {
|
||||||
hide: false,
|
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;
|
||||||
|
|
|
@ -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 { _t, _tJsx} from '../../../languageHandler';
|
import AppsDrawer from './AppsDrawer';
|
||||||
|
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 }
|
||||||
|
|
|
@ -13,16 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
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';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var CallHandler = require('../../../CallHandler');
|
import CallHandler from '../../../CallHandler';
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var Modal = require('../../../Modal');
|
import Modal from '../../../Modal';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var dis = require('../../../dispatcher');
|
import dis from '../../../dispatcher';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +30,8 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onCallClick = this.onCallClick.bind(this);
|
this.onCallClick = this.onCallClick.bind(this);
|
||||||
this.onHangupClick = this.onHangupClick.bind(this);
|
this.onHangupClick = this.onHangupClick.bind(this);
|
||||||
this.onUploadClick = this.onUploadClick.bind(this);
|
this.onUploadClick = this.onUploadClick.bind(this);
|
||||||
|
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||||
|
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||||
this.uploadFiles = this.uploadFiles.bind(this);
|
this.uploadFiles = this.uploadFiles.bind(this);
|
||||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||||
|
@ -57,7 +57,6 @@ export default class MessageComposer extends React.Component {
|
||||||
},
|
},
|
||||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -127,7 +126,7 @@ export default class MessageComposer extends React.Component {
|
||||||
if(shouldUpload) {
|
if(shouldUpload) {
|
||||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||||
if (files) {
|
if (files) {
|
||||||
for(var i=0; i<files.length; i++) {
|
for(let i=0; i<files.length; i++) {
|
||||||
this.props.uploadFile(files[i]);
|
this.props.uploadFile(files[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +138,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onHangupClick() {
|
onHangupClick() {
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||||
//var call = CallHandler.getAnyActiveCall();
|
//var call = CallHandler.getAnyActiveCall();
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return;
|
return;
|
||||||
|
@ -152,20 +151,68 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _startCallApp(isAudioConf) {
|
||||||
|
// dis.dispatch({
|
||||||
|
// action: 'appsDrawer',
|
||||||
|
// show: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
// let appsStateEvent = {};
|
||||||
|
// if (appsStateEvents) {
|
||||||
|
// appsStateEvent = appsStateEvents.getContent();
|
||||||
|
// }
|
||||||
|
// if (!appsStateEvent.videoConf) {
|
||||||
|
// appsStateEvent.videoConf = {
|
||||||
|
// type: 'jitsi',
|
||||||
|
// // FIXME -- This should not be localhost
|
||||||
|
// url: 'http://localhost:8000/jitsi.html',
|
||||||
|
// data: {
|
||||||
|
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||||
|
// isAudioConf: isAudioConf,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// MatrixClientPeg.get().sendStateEvent(
|
||||||
|
// this.props.room.roomId,
|
||||||
|
// 'im.vector.modular.widgets',
|
||||||
|
// appsStateEvent,
|
||||||
|
// '',
|
||||||
|
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
onCallClick(ev) {
|
onCallClick(ev) {
|
||||||
|
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: ev.shiftKey ? "screensharing" : "video",
|
type: ev.shiftKey ? "screensharing" : "video",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
|
// this._startCallApp(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceCallClick(ev) {
|
onVoiceCallClick(ev) {
|
||||||
|
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: 'voice',
|
type: "voice",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
|
// this._startCallApp(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowAppsClick(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onHideAppsClick(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||||
|
@ -216,19 +263,19 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var uploadInputStyle = {display: 'none'};
|
const uploadInputStyle = {display: 'none'};
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||||
|
|
||||||
var controls = [];
|
const controls = [];
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||||
<MemberAvatar member={me} width={24} height={24} />
|
<MemberAvatar member={me} width={24} height={24} />
|
||||||
</div>
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
let e2eImg, e2eTitle, e2eClass;
|
let e2eImg, e2eTitle, e2eClass;
|
||||||
|
@ -247,16 +294,15 @@ export default class MessageComposer extends React.Component {
|
||||||
controls.push(
|
controls.push(
|
||||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||||
alt={e2eTitle} title={e2eTitle}
|
alt={e2eTitle} title={e2eTitle}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
var callButton, videoCallButton, hangupButton;
|
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||||
if (this.props.callState && this.props.callState !== 'ended') {
|
if (this.props.callState && this.props.callState !== 'ended') {
|
||||||
hangupButton =
|
hangupButton =
|
||||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||||
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
callButton =
|
callButton =
|
||||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
||||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||||
|
@ -267,14 +313,29 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
// Apps
|
||||||
|
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||||
|
if (this.props.showApps) {
|
||||||
|
hideAppsButton =
|
||||||
|
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||||
|
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
showAppsButton =
|
||||||
|
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||||
|
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||||
MatrixClientPeg.get().credentials.userId);
|
MatrixClientPeg.get().credentials.userId);
|
||||||
|
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
// This also currently includes the call buttons. Really we should
|
// This also currently includes the call buttons. Really we should
|
||||||
// check separately for whether we can call, but this is slightly
|
// check separately for whether we can call, but this is slightly
|
||||||
// complex because of conference calls.
|
// complex because of conference calls.
|
||||||
var uploadButton = (
|
const uploadButton = (
|
||||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||||
|
@ -300,7 +361,7 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
ref={c => this.messageComposerInput = c}
|
ref={(c) => this.messageComposerInput = c}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
@ -316,13 +377,15 @@ export default class MessageComposer extends React.Component {
|
||||||
uploadButton,
|
uploadButton,
|
||||||
hangupButton,
|
hangupButton,
|
||||||
callButton,
|
callButton,
|
||||||
videoCallButton
|
videoCallButton,
|
||||||
|
showAppsButton,
|
||||||
|
hideAppsButton,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
controls.push(
|
||||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||||
{ _t('You do not have permission to post to this room') }
|
{ _t('You do not have permission to post to this room') }
|
||||||
</div>
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,18 +403,14 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
const {style, blockType} = this.state.inputState;
|
const {style, blockType} = this.state.inputState;
|
||||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||||
name => {
|
(name) => {
|
||||||
const active = style.includes(name) || blockType === name;
|
const active = style.includes(name) || blockType === name;
|
||||||
const suffix = active ? '-o-n' : '';
|
const suffix = active ? '-o-n' : '';
|
||||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||||
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||||
const className = classNames("mx_MessageComposer_format_button", {
|
|
||||||
mx_MessageComposer_format_button_disabled: disabled,
|
|
||||||
mx_filterFlipColor: true,
|
|
||||||
});
|
|
||||||
return <img className={className}
|
return <img className={className}
|
||||||
title={ _t(name) }
|
title={ _t(name) }
|
||||||
onMouseDown={disabled ? null : onFormatButtonClicked}
|
onMouseDown={onFormatButtonClicked}
|
||||||
key={name}
|
key={name}
|
||||||
src={`img/button-text-${name}${suffix}.svg`}
|
src={`img/button-text-${name}${suffix}.svg`}
|
||||||
height="17" />;
|
height="17" />;
|
||||||
|
@ -403,5 +462,8 @@ MessageComposer.propTypes = {
|
||||||
uploadFile: React.PropTypes.func.isRequired,
|
uploadFile: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
// opacity for dynamic UI fading effects
|
// opacity for dynamic UI fading effects
|
||||||
opacity: React.PropTypes.number
|
opacity: React.PropTypes.number,
|
||||||
|
|
||||||
|
// string representing the current room app drawer state
|
||||||
|
showApps: React.PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,6 +43,8 @@ import Markdown from '../../../Markdown';
|
||||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||||
import {onSendMessageFailed} from './MessageComposerInputOld';
|
import {onSendMessageFailed} from './MessageComposerInputOld';
|
||||||
|
|
||||||
|
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const ZWS_CODE = 8203;
|
const ZWS_CODE = 8203;
|
||||||
|
@ -87,6 +89,13 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return 'toggle-mode';
|
return 'toggle-mode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
|
||||||
|
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
|
||||||
|
// When null is returned, draft-js will NOT preventDefault, allowing dev tools
|
||||||
|
// to be toggled when the editor is focussed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return getDefaultKeyBinding(e);
|
return getDefaultKeyBinding(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +123,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.onEscape = this.onEscape.bind(this);
|
this.onEscape = this.onEscape.bind(this);
|
||||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
|
this.onTextPasted = this.onTextPasted.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||||
|
|
||||||
|
@ -122,15 +132,21 @@ export default class MessageComposerInput extends React.Component {
|
||||||
isRichtextEnabled,
|
isRichtextEnabled,
|
||||||
|
|
||||||
// the currently displayed editor state (note: this is always what is modified on input)
|
// the currently displayed editor state (note: this is always what is modified on input)
|
||||||
editorState: null,
|
editorState: this.createEditorState(
|
||||||
|
isRichtextEnabled,
|
||||||
|
MessageComposerStore.getContentState(this.props.room.roomId),
|
||||||
|
),
|
||||||
|
|
||||||
// the original editor state, before we started tabbing through completions
|
// the original editor state, before we started tabbing through completions
|
||||||
originalEditorState: null,
|
originalEditorState: null,
|
||||||
};
|
|
||||||
|
|
||||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
// the virtual state "above" the history stack, the message currently being composed that
|
||||||
/* eslint react/no-direct-mutation-state:0 */
|
// we want to persist whilst browsing history
|
||||||
this.state.editorState = this.createEditorState();
|
currentlyComposedEditorState: null,
|
||||||
|
|
||||||
|
// whether there were any completions
|
||||||
|
someCompletions: null,
|
||||||
|
};
|
||||||
|
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
@ -217,7 +233,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
||||||
}
|
}
|
||||||
const editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
|
editorState = EditorState.moveSelectionToEnd(editorState);
|
||||||
this.onEditorContentChanged(editorState);
|
this.onEditorContentChanged(editorState);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
|
@ -323,6 +340,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.onFinishedTyping();
|
this.onFinishedTyping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record the editor state for this room so that it can be retrieved after
|
||||||
|
// switching to another room and back
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'content_state',
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
content_state: state.editorState.getCurrentContent(),
|
||||||
|
});
|
||||||
|
|
||||||
if (!state.hasOwnProperty('originalEditorState')) {
|
if (!state.hasOwnProperty('originalEditorState')) {
|
||||||
state.originalEditorState = null;
|
state.originalEditorState = null;
|
||||||
}
|
}
|
||||||
|
@ -390,26 +415,59 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let contentState = this.state.editorState.getCurrentContent(),
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
selection = this.state.editorState.getSelection();
|
|
||||||
|
|
||||||
const modifyFn = {
|
const modifyFn = {
|
||||||
'bold': (text) => `**${text}**`,
|
'bold': (text) => `**${text}**`,
|
||||||
'italic': (text) => `*${text}*`,
|
'italic': (text) => `*${text}*`,
|
||||||
'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
'underline': (text) => `<u>${text}</u>`,
|
||||||
'strike': (text) => `<del>${text}</del>`,
|
'strike': (text) => `<del>${text}</del>`,
|
||||||
'code-block': (text) => `\`\`\`\n${text}\n\`\`\``,
|
'code-block': (text) => `\`\`\`\n${text}\n\`\`\`\n`,
|
||||||
'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''),
|
'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join('') + '\n',
|
||||||
'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''),
|
'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''),
|
||||||
'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''),
|
'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''),
|
||||||
}[command];
|
}[command];
|
||||||
|
|
||||||
|
const selectionAfterOffset = {
|
||||||
|
'bold': -2,
|
||||||
|
'italic': -1,
|
||||||
|
'underline': -4,
|
||||||
|
'strike': -6,
|
||||||
|
'code-block': -5,
|
||||||
|
'blockquote': -2,
|
||||||
|
}[command];
|
||||||
|
|
||||||
|
// Returns a function that collapses a selectionState to its end and moves it by offset
|
||||||
|
const collapseAndOffsetSelection = (selectionState, offset) => {
|
||||||
|
const key = selectionState.getEndKey();
|
||||||
|
return new SelectionState({
|
||||||
|
anchorKey: key, anchorOffset: offset,
|
||||||
|
focusKey: key, focusOffset: offset,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (modifyFn) {
|
if (modifyFn) {
|
||||||
|
const previousSelection = this.state.editorState.getSelection();
|
||||||
|
const newContentState = RichText.modifyText(contentState, previousSelection, modifyFn);
|
||||||
newState = EditorState.push(
|
newState = EditorState.push(
|
||||||
this.state.editorState,
|
this.state.editorState,
|
||||||
RichText.modifyText(contentState, selection, modifyFn),
|
newContentState,
|
||||||
'insert-characters',
|
'insert-characters',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let newSelection = newContentState.getSelectionAfter();
|
||||||
|
// If the selection range is 0, move the cursor inside the formatted body
|
||||||
|
if (previousSelection.getStartOffset() === previousSelection.getEndOffset() &&
|
||||||
|
previousSelection.getStartKey() === previousSelection.getEndKey() &&
|
||||||
|
selectionAfterOffset !== undefined
|
||||||
|
) {
|
||||||
|
const selectedBlock = newContentState.getBlockForKey(previousSelection.getAnchorKey());
|
||||||
|
const blockLength = selectedBlock.getText().length;
|
||||||
|
const newOffset = blockLength + selectionAfterOffset;
|
||||||
|
newSelection = collapseAndOffsetSelection(newSelection, newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
newState = EditorState.forceSelection(newState, newSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +483,28 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTextPasted(text: string, html?: string) {
|
||||||
|
const currentSelection = this.state.editorState.getSelection();
|
||||||
|
const currentContent = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
|
let contentState = null;
|
||||||
|
if (html && this.state.isRichtextEnabled) {
|
||||||
|
contentState = Modifier.replaceWithFragment(
|
||||||
|
currentContent,
|
||||||
|
currentSelection,
|
||||||
|
RichText.htmlToContentState(html).getBlockMap(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
contentState = Modifier.replaceText(currentContent, currentSelection, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newEditorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
|
|
||||||
|
newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
|
||||||
|
this.onEditorContentChanged(newEditorState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
|
@ -476,9 +556,30 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
contentHTML = HtmlUtils.stripParagraphs(
|
// We should only send HTML if any block is styled or contains inline style
|
||||||
RichText.contentStateToHTML(contentState),
|
let shouldSendHTML = false;
|
||||||
);
|
const blocks = contentState.getBlocksAsArray();
|
||||||
|
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||||
|
shouldSendHTML = true;
|
||||||
|
} else {
|
||||||
|
const characterLists = blocks.map((block) => block.getCharacterList());
|
||||||
|
// For each block of characters, determine if any inline styles are applied
|
||||||
|
// and if yes, send HTML
|
||||||
|
characterLists.forEach((characters) => {
|
||||||
|
const numberOfStylesForCharacters = characters.map(
|
||||||
|
(character) => character.getStyle().toArray().length,
|
||||||
|
).toArray();
|
||||||
|
// If any character has more than 0 inline styles applied, send HTML
|
||||||
|
if (numberOfStylesForCharacters.some((styles) => styles > 0)) {
|
||||||
|
shouldSendHTML = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (shouldSendHTML) {
|
||||||
|
contentHTML = HtmlUtils.processHtmlForSending(
|
||||||
|
RichText.contentStateToHTML(contentState),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const md = new Markdown(contentText);
|
const md = new Markdown(contentText);
|
||||||
if (md.isPlainText()) {
|
if (md.isPlainText()) {
|
||||||
|
@ -491,6 +592,16 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let sendHtmlFn = this.client.sendHtmlMessage;
|
let sendHtmlFn = this.client.sendHtmlMessage;
|
||||||
let sendTextFn = this.client.sendTextMessage;
|
let sendTextFn = this.client.sendTextMessage;
|
||||||
|
|
||||||
|
if (this.state.isRichtextEnabled) {
|
||||||
|
this.historyManager.addItem(
|
||||||
|
contentHTML ? contentHTML : contentText,
|
||||||
|
contentHTML ? 'html' : 'markdown',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Always store MD input as input history
|
||||||
|
this.historyManager.addItem(contentText, 'markdown');
|
||||||
|
}
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
contentText = contentText.substring(4);
|
contentText = contentText.substring(4);
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// bit of a hack, but the alternative would be quite complicated
|
||||||
|
@ -499,10 +610,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.historyManager.addItem(
|
|
||||||
this.state.isRichtextEnabled ? contentHTML : contentState.getPlainText(),
|
|
||||||
this.state.isRichtextEnabled ? 'html' : 'markdown');
|
|
||||||
|
|
||||||
let sendMessagePromise;
|
let sendMessagePromise;
|
||||||
if (contentHTML) {
|
if (contentHTML) {
|
||||||
sendMessagePromise = sendHtmlFn.call(
|
sendMessagePromise = sendHtmlFn.call(
|
||||||
|
@ -525,49 +632,117 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.autocomplete.hide();
|
this.autocomplete.hide();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpArrow = (e) => {
|
||||||
|
this.onVerticalArrow(e, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpArrow = async (e) => {
|
onDownArrow = (e) => {
|
||||||
const completion = this.autocomplete.onUpArrow();
|
this.onVerticalArrow(e, false);
|
||||||
if (completion == null) {
|
};
|
||||||
const newContent = this.historyManager.getItem(-1, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
|
||||||
if (!newContent) return false;
|
onVerticalArrow = (e, up) => {
|
||||||
const editorState = EditorState.push(this.state.editorState,
|
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
|
||||||
newContent,
|
return;
|
||||||
'insert-characters');
|
|
||||||
this.setState({editorState});
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
return await this.setDisplayedCompletion(completion);
|
|
||||||
};
|
|
||||||
|
|
||||||
onDownArrow = async (e) => {
|
// Select history only if we are not currently auto-completing
|
||||||
const completion = this.autocomplete.onDownArrow();
|
|
||||||
if (completion == null) {
|
|
||||||
const newContent = this.historyManager.getItem(+1, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
|
||||||
if (!newContent) return false;
|
|
||||||
const editorState = EditorState.push(this.state.editorState,
|
|
||||||
newContent,
|
|
||||||
'insert-characters');
|
|
||||||
this.setState({editorState});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
return await this.setDisplayedCompletion(completion);
|
|
||||||
};
|
|
||||||
|
|
||||||
// tab and shift-tab are mapped to down and up arrow respectively
|
|
||||||
onTab = async (e) => {
|
|
||||||
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
|
|
||||||
if (this.autocomplete.state.completionList.length === 0) {
|
if (this.autocomplete.state.completionList.length === 0) {
|
||||||
await this.autocomplete.forceComplete();
|
// Don't go back in history if we're in the middle of a multi-line message
|
||||||
this.onDownArrow(e);
|
const selection = this.state.editorState.getSelection();
|
||||||
|
const blockKey = selection.getStartKey();
|
||||||
|
const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock();
|
||||||
|
const lastBlock = this.state.editorState.getCurrentContent().getLastBlock();
|
||||||
|
|
||||||
|
let canMoveUp = false;
|
||||||
|
let canMoveDown = false;
|
||||||
|
if (blockKey === firstBlock.getKey()) {
|
||||||
|
canMoveUp = selection.getStartOffset() === selection.getEndOffset() &&
|
||||||
|
selection.getStartOffset() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockKey === lastBlock.getKey()) {
|
||||||
|
canMoveDown = selection.getStartOffset() === selection.getEndOffset() &&
|
||||||
|
selection.getStartOffset() === lastBlock.getText().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
|
||||||
|
|
||||||
|
const selected = this.selectHistory(up);
|
||||||
|
if (selected) {
|
||||||
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
|
this.moveAutocompleteSelection(up);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectHistory = async (up) => {
|
||||||
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
// True if we are not currently selecting history, but composing a message
|
||||||
|
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||||
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
if (!up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
currentlyComposedEditorState: this.state.editorState,
|
||||||
|
});
|
||||||
|
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||||
|
// True when we return to the message being composed currently
|
||||||
|
this.setState({
|
||||||
|
editorState: this.state.currentlyComposedEditorState,
|
||||||
|
});
|
||||||
|
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
||||||
|
if (!newContent) return false;
|
||||||
|
let editorState = EditorState.push(
|
||||||
|
this.state.editorState,
|
||||||
|
newContent,
|
||||||
|
'insert-characters',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move selection to the end of the selected history
|
||||||
|
let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey());
|
||||||
|
newSelection = newSelection.merge({
|
||||||
|
focusOffset: newContent.getLastBlock().getLength(),
|
||||||
|
anchorOffset: newContent.getLastBlock().getLength(),
|
||||||
|
});
|
||||||
|
editorState = EditorState.forceSelection(editorState, newSelection);
|
||||||
|
|
||||||
|
this.setState({editorState});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onTab = async (e) => {
|
||||||
|
this.setState({
|
||||||
|
someCompletions: null,
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.autocomplete.state.completionList.length === 0) {
|
||||||
|
// Force completions to show for the text currently entered
|
||||||
|
const completionCount = await this.autocomplete.forceComplete();
|
||||||
|
this.setState({
|
||||||
|
someCompletions: completionCount > 0,
|
||||||
|
});
|
||||||
|
// Select the first item by moving "down"
|
||||||
|
await this.moveAutocompleteSelection(false);
|
||||||
|
} else {
|
||||||
|
await this.moveAutocompleteSelection(e.shiftKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
moveAutocompleteSelection = (up) => {
|
||||||
|
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||||
|
return this.setDisplayedCompletion(completion);
|
||||||
|
};
|
||||||
|
|
||||||
onEscape = async (e) => {
|
onEscape = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.autocomplete) {
|
if (this.autocomplete) {
|
||||||
|
@ -676,6 +851,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const className = classNames('mx_MessageComposer_input', {
|
const className = classNames('mx_MessageComposer_input', {
|
||||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||||
|
mx_MessageComposer_input_error: this.state.someCompletions === false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = activeEditorState.getCurrentContent();
|
const content = activeEditorState.getCurrentContent();
|
||||||
|
@ -706,6 +882,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
|
handlePastedText={this.onTextPasted}
|
||||||
handlePastedFiles={this.props.onFilesPasted}
|
handlePastedFiles={this.props.onFilesPasted}
|
||||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||||
onTab={this.onTab}
|
onTab={this.onTab}
|
||||||
|
|
|
@ -16,18 +16,18 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var Modal = require("../../../Modal");
|
import Modal from "../../../Modal";
|
||||||
var dis = require("../../../dispatcher");
|
import dis from "../../../dispatcher";
|
||||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
var linkify = require('linkifyjs');
|
import * as linkify from 'linkifyjs';
|
||||||
var linkifyElement = require('linkifyjs/element');
|
import linkifyElement from 'linkifyjs/element';
|
||||||
var linkifyMatrix = require('../../../linkify-matrix');
|
import linkifyMatrix from '../../../linkify-matrix';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
import {CancelButton} from './SimpleRoomHeader';
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||||
|
|
||||||
// When a room name occurs, RoomState.events is fired *before*
|
// When a room name occurs, RoomState.events is fired *before*
|
||||||
|
@ -79,14 +79,14 @@ module.exports = React.createClass({
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
this.props.room.removeListener("Room.name", this._onRoomNameChange);
|
this.props.room.removeListener("Room.name", this._onRoomNameChange);
|
||||||
}
|
}
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRoomStateEvents: function(event, state) {
|
_onRoomStateEvents: function(event, state) {
|
||||||
if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
|
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,8 @@ module.exports = React.createClass({
|
||||||
this._rateLimitedUpdate();
|
this._rateLimitedUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
_rateLimitedUpdate: new rate_limited_func(function() {
|
_rateLimitedUpdate: new RateLimitedFunc(function() {
|
||||||
|
/* eslint-disable babel/no-invalid-this */
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
|
@ -109,15 +110,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAvatarSelected: function(ev) {
|
onAvatarSelected: function(ev) {
|
||||||
var self = this;
|
const changeAvatar = this.refs.changeAvatar;
|
||||||
var changeAvatar = this.refs.changeAvatar;
|
|
||||||
if (!changeAvatar) {
|
if (!changeAvatar) {
|
||||||
console.error("No ChangeAvatar found to upload image to!");
|
console.error("No ChangeAvatar found to upload image to!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to set avatar: " + errMsg);
|
console.error("Failed to set avatar: " + errMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
|
@ -133,10 +133,10 @@ module.exports = React.createClass({
|
||||||
/**
|
/**
|
||||||
* After editing the settings, get the new name for the room
|
* After editing the settings, get the new name for the room
|
||||||
*
|
*
|
||||||
* Returns undefined if we didn't let the user edit the room name
|
* @return {?string} newName or undefined if we didn't let the user edit the room name
|
||||||
*/
|
*/
|
||||||
getEditedName: function() {
|
getEditedName: function() {
|
||||||
var newName;
|
let newName;
|
||||||
if (this.refs.nameEditor) {
|
if (this.refs.nameEditor) {
|
||||||
newName = this.refs.nameEditor.getRoomName();
|
newName = this.refs.nameEditor.getRoomName();
|
||||||
}
|
}
|
||||||
|
@ -146,10 +146,10 @@ module.exports = React.createClass({
|
||||||
/**
|
/**
|
||||||
* After editing the settings, get the new topic for the room
|
* After editing the settings, get the new topic for the room
|
||||||
*
|
*
|
||||||
* Returns undefined if we didn't let the user edit the room topic
|
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
|
||||||
*/
|
*/
|
||||||
getEditedTopic: function() {
|
getEditedTopic: function() {
|
||||||
var newTopic;
|
let newTopic;
|
||||||
if (this.refs.topicEditor) {
|
if (this.refs.topicEditor) {
|
||||||
newTopic = this.refs.topicEditor.getTopic();
|
newTopic = this.refs.topicEditor.getTopic();
|
||||||
}
|
}
|
||||||
|
@ -157,38 +157,31 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||||
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
|
||||||
var header;
|
let name = null;
|
||||||
var name = null;
|
let searchStatus = null;
|
||||||
var searchStatus = null;
|
let topicElement = null;
|
||||||
var topic_el = null;
|
let cancelButton = null;
|
||||||
var cancel_button = null;
|
let spinner = null;
|
||||||
var spinner = null;
|
let saveButton = null;
|
||||||
var save_button = null;
|
let settingsButton = null;
|
||||||
var settings_button = null;
|
|
||||||
|
let canSetRoomName;
|
||||||
|
let canSetRoomAvatar;
|
||||||
|
let canSetRoomTopic;
|
||||||
if (this.props.editing) {
|
if (this.props.editing) {
|
||||||
|
|
||||||
// calculate permissions. XXX: this should be done on mount or something
|
// calculate permissions. XXX: this should be done on mount or something
|
||||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
const userId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
|
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
|
||||||
'm.room.name', user_id
|
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
|
||||||
);
|
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
|
||||||
var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
|
|
||||||
'm.room.avatar', user_id
|
|
||||||
);
|
|
||||||
var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
|
|
||||||
'm.room.topic', user_id
|
|
||||||
);
|
|
||||||
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
|
|
||||||
'm.room.name', user_id
|
|
||||||
);
|
|
||||||
|
|
||||||
save_button = (
|
saveButton = (
|
||||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||||
{_t("Save")}
|
{_t("Save")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -196,39 +189,41 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.saving) {
|
if (this.props.saving) {
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (can_set_room_name) {
|
if (canSetRoomName) {
|
||||||
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
var searchStatus;
|
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a valid (possibly zero) searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
|
if (this.props.searchInfo &&
|
||||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
|
this.props.searchInfo.searchCount !== undefined &&
|
||||||
|
this.props.searchInfo.searchCount !== null) {
|
||||||
|
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||||
|
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||||
var settingsHint = false;
|
let settingsHint = false;
|
||||||
var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||||
if (members) {
|
if (members) {
|
||||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||||
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||||
if (!name || !name.getContent().name) {
|
if (!nameEvent || !nameEvent.getContent().name) {
|
||||||
settingsHint = true;
|
settingsHint = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomName = _t("Join Room");
|
let roomName = _t("Join Room");
|
||||||
if (this.props.oobData && this.props.oobData.name) {
|
if (this.props.oobData && this.props.oobData.name) {
|
||||||
roomName = this.props.oobData.name;
|
roomName = this.props.oobData.name;
|
||||||
} else if (this.props.room) {
|
} else if (this.props.room) {
|
||||||
|
@ -243,24 +238,25 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (can_set_room_topic) {
|
if (canSetRoomTopic) {
|
||||||
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||||
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||||
} else {
|
} else {
|
||||||
var topic;
|
let topic;
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||||
if (ev) {
|
if (ev) {
|
||||||
topic = ev.getContent().topic;
|
topic = ev.getContent().topic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (topic) {
|
if (topic) {
|
||||||
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
topicElement =
|
||||||
|
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomAvatar = null;
|
let roomAvatar = null;
|
||||||
if (can_set_room_avatar) {
|
if (canSetRoomAvatar) {
|
||||||
roomAvatar = (
|
roomAvatar = (
|
||||||
<div className="mx_RoomHeader_avatarPicker">
|
<div className="mx_RoomHeader_avatarPicker">
|
||||||
<div onClick={ this.onAvatarPickerClick }>
|
<div onClick={ this.onAvatarPickerClick }>
|
||||||
|
@ -276,8 +272,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
||||||
else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
|
||||||
roomAvatar = (
|
roomAvatar = (
|
||||||
<div onClick={this.props.onSettingsClick}>
|
<div onClick={this.props.onSettingsClick}>
|
||||||
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
|
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
|
||||||
|
@ -285,9 +280,8 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var settings_button;
|
|
||||||
if (this.props.onSettingsClick) {
|
if (this.props.onSettingsClick) {
|
||||||
settings_button =
|
settingsButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -301,61 +295,58 @@ module.exports = React.createClass({
|
||||||
// </div>;
|
// </div>;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var forget_button;
|
let forgetButton;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forget_button =
|
forgetButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
|
||||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let search_button;
|
let searchButton;
|
||||||
if (this.props.onSearchClick && this.props.inRoom) {
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
search_button =
|
searchButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
|
||||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rightPanel_buttons;
|
let rightPanelButtons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanel_buttons =
|
rightPanelButtons =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
|
||||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var right_row;
|
let rightRow;
|
||||||
if (!this.props.editing) {
|
if (!this.props.editing) {
|
||||||
right_row =
|
rightRow =
|
||||||
<div className="mx_RoomHeader_rightRow">
|
<div className="mx_RoomHeader_rightRow">
|
||||||
{ settings_button }
|
{ settingsButton }
|
||||||
{ forget_button }
|
{ forgetButton }
|
||||||
{ search_button }
|
{ searchButton }
|
||||||
{ rightPanel_buttons }
|
{ rightPanelButtons }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
header =
|
|
||||||
<div className="mx_RoomHeader_wrapper">
|
|
||||||
<div className="mx_RoomHeader_leftRow">
|
|
||||||
<div className="mx_RoomHeader_avatar">
|
|
||||||
{ roomAvatar }
|
|
||||||
</div>
|
|
||||||
<div className="mx_RoomHeader_info">
|
|
||||||
{ name }
|
|
||||||
{ topic_el }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{spinner}
|
|
||||||
{save_button}
|
|
||||||
{cancel_button}
|
|
||||||
{right_row}
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||||
{ header }
|
<div className="mx_RoomHeader_wrapper">
|
||||||
|
<div className="mx_RoomHeader_leftRow">
|
||||||
|
<div className="mx_RoomHeader_avatar">
|
||||||
|
{ roomAvatar }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomHeader_info">
|
||||||
|
{ name }
|
||||||
|
{ topicElement }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{spinner}
|
||||||
|
{saveButton}
|
||||||
|
{cancelButton}
|
||||||
|
{rightRow}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) {
|
||||||
|
|
||||||
const BannedUser = React.createClass({
|
const BannedUser = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
canUnban: React.PropTypes.bool,
|
||||||
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
|
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
|
||||||
reason: React.PropTypes.string,
|
reason: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
@ -67,13 +68,17 @@ const BannedUser = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
let unbanButton;
|
||||||
|
|
||||||
|
if (this.props.canUnban) {
|
||||||
|
unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
|
||||||
|
{ _t('Unban') }
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton className="mx_RoomSettings_unbanButton"
|
{ unbanButton }
|
||||||
onClick={this._onUnbanClick}
|
|
||||||
>
|
|
||||||
{ _t('Unban') }
|
|
||||||
</AccessibleButton>
|
|
||||||
<strong>{this.props.member.name}</strong> {this.props.member.userId}
|
<strong>{this.props.member.name}</strong> {this.props.member.userId}
|
||||||
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
|
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
|
||||||
</li>
|
</li>
|
||||||
|
@ -667,6 +672,7 @@ module.exports = React.createClass({
|
||||||
const banned = this.props.room.getMembersWithMembership("ban");
|
const banned = this.props.room.getMembersWithMembership("ban");
|
||||||
let bannedUsersSection;
|
let bannedUsersSection;
|
||||||
if (banned.length) {
|
if (banned.length) {
|
||||||
|
const canBanUsers = current_user_level >= ban_level;
|
||||||
bannedUsersSection =
|
bannedUsersSection =
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t('Banned users') }</h3>
|
<h3>{ _t('Banned users') }</h3>
|
||||||
|
@ -674,7 +680,7 @@ module.exports = React.createClass({
|
||||||
{banned.map(function(member) {
|
{banned.map(function(member) {
|
||||||
const banEvent = member.events.member.getContent();
|
const banEvent = member.events.member.getContent();
|
||||||
return (
|
return (
|
||||||
<BannedUser key={member.userId} member={member} reason={banEvent.reason} />
|
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -13,11 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
var CallHandler = require("../../../CallHandler");
|
import CallHandler from '../../../CallHandler';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -73,10 +73,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
showCall: function() {
|
showCall: function() {
|
||||||
var call;
|
let call;
|
||||||
|
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
var roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
call = CallHandler.getCallForRoom(roomId) ||
|
call = CallHandler.getCallForRoom(roomId) ||
|
||||||
(this.props.ConferenceHandler ?
|
(this.props.ConferenceHandler ?
|
||||||
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
|
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
|
||||||
|
@ -86,9 +86,7 @@ module.exports = React.createClass({
|
||||||
if (this.call) {
|
if (this.call) {
|
||||||
this.setState({ call: call });
|
this.setState({ call: call });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
call = CallHandler.getAnyActiveCall();
|
call = CallHandler.getAnyActiveCall();
|
||||||
this.setState({ call: call });
|
this.setState({ call: call });
|
||||||
}
|
}
|
||||||
|
@ -109,8 +107,7 @@ module.exports = React.createClass({
|
||||||
call.confUserId ? "none" : "block"
|
call.confUserId ? "none" : "block"
|
||||||
);
|
);
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
||||||
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
||||||
|
@ -126,11 +123,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var VideoView = sdk.getComponent('voip.VideoView');
|
const VideoView = sdk.getComponent('voip.VideoView');
|
||||||
|
|
||||||
var voice;
|
let voice;
|
||||||
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
|
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
|
||||||
var callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
|
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
|
||||||
voice = (
|
voice = (
|
||||||
<div className="mx_CallView_voice" onClick={ this.props.onClick }>
|
<div className="mx_CallView_voice" onClick={ this.props.onClick }>
|
||||||
{_t("Active call (%(roomName)s)", {roomName: callRoom.name})}
|
{_t("Active call (%(roomName)s)", {roomName: callRoom.name})}
|
||||||
|
@ -147,6 +144,6 @@ module.exports = React.createClass({
|
||||||
{ voice }
|
{ voice }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
var CallHandler = require("../../../CallHandler");
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -29,34 +28,32 @@ module.exports = React.createClass({
|
||||||
onAnswerClick: function() {
|
onAnswerClick: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'answer',
|
action: 'answer',
|
||||||
room_id: this.props.incomingCall.roomId
|
room_id: this.props.incomingCall.roomId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onRejectClick: function() {
|
onRejectClick: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'hangup',
|
action: 'hangup',
|
||||||
room_id: this.props.incomingCall.roomId
|
room_id: this.props.incomingCall.roomId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var room = null;
|
let room = null;
|
||||||
if (this.props.incomingCall) {
|
if (this.props.incomingCall) {
|
||||||
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
|
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var caller = room ? room.name : _t("unknown caller");
|
const caller = room ? room.name : _t("unknown caller");
|
||||||
|
|
||||||
let incomingCallText = null;
|
let incomingCallText = null;
|
||||||
if (this.props.incomingCall) {
|
if (this.props.incomingCall) {
|
||||||
if (this.props.incomingCall.type === "voice") {
|
if (this.props.incomingCall.type === "voice") {
|
||||||
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
|
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
|
||||||
}
|
} else if (this.props.incomingCall.type === "video") {
|
||||||
else if (this.props.incomingCall.type === "video") {
|
|
||||||
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
|
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
|
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +78,6 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoFeed',
|
displayName: 'VideoFeed',
|
||||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require('react-dom');
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var dis = require('../../../dispatcher');
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoView',
|
displayName: 'VideoView',
|
||||||
|
@ -53,9 +53,10 @@ module.exports = React.createClass({
|
||||||
// this needs to be somewhere at the top of the DOM which
|
// this needs to be somewhere at the top of the DOM which
|
||||||
// always exists to avoid audio interruptions.
|
// always exists to avoid audio interruptions.
|
||||||
// Might as well just use DOM.
|
// Might as well just use DOM.
|
||||||
var remoteAudioElement = document.getElementById("remoteAudio");
|
const remoteAudioElement = document.getElementById("remoteAudio");
|
||||||
if (!remoteAudioElement) {
|
if (!remoteAudioElement) {
|
||||||
console.error("Failed to find remoteAudio element - cannot play audio! You need to add an <audio/> to the DOM.");
|
console.error("Failed to find remoteAudio element - cannot play audio!"
|
||||||
|
+ "You need to add an <audio/> to the DOM.");
|
||||||
}
|
}
|
||||||
return remoteAudioElement;
|
return remoteAudioElement;
|
||||||
},
|
},
|
||||||
|
@ -70,22 +71,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'video_fullscreen':
|
case 'video_fullscreen': {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var element = this.container;
|
const element = this.container;
|
||||||
if (payload.fullscreen) {
|
if (payload.fullscreen) {
|
||||||
var requestMethod = (
|
const requestMethod = (
|
||||||
element.requestFullScreen ||
|
element.requestFullScreen ||
|
||||||
element.webkitRequestFullScreen ||
|
element.webkitRequestFullScreen ||
|
||||||
element.mozRequestFullScreen ||
|
element.mozRequestFullScreen ||
|
||||||
element.msRequestFullscreen
|
element.msRequestFullscreen
|
||||||
);
|
);
|
||||||
requestMethod.call(element);
|
requestMethod.call(element);
|
||||||
}
|
} else {
|
||||||
else {
|
const exitMethod = (
|
||||||
var exitMethod = (
|
|
||||||
document.exitFullscreen ||
|
document.exitFullscreen ||
|
||||||
document.mozCancelFullScreen ||
|
document.mozCancelFullScreen ||
|
||||||
document.webkitExitFullscreen ||
|
document.webkitExitFullscreen ||
|
||||||
|
@ -96,17 +96,18 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var VideoFeed = sdk.getComponent('voip.VideoFeed');
|
const VideoFeed = sdk.getComponent('voip.VideoFeed');
|
||||||
|
|
||||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||||
var fullscreenElement = (document.fullscreenElement ||
|
const fullscreenElement = (document.fullscreenElement ||
|
||||||
document.mozFullScreenElement ||
|
document.mozFullScreenElement ||
|
||||||
document.webkitFullscreenElement);
|
document.webkitFullscreenElement);
|
||||||
var maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||||
|
@ -119,5 +120,5 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,24 +14,24 @@ 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 Modal = require('./Modal');
|
import Modal from './Modal';
|
||||||
var sdk = require('./index');
|
import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
var dis = require("./dispatcher");
|
import dis from "./dispatcher";
|
||||||
var Rooms = require("./Rooms");
|
import * as Rooms from "./Rooms";
|
||||||
|
|
||||||
var q = require('q');
|
import q from 'q';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new room, and switch to it.
|
* Create a new room, and switch to it.
|
||||||
*
|
*
|
||||||
* Returns a promise which resolves to the room id, or null if the
|
|
||||||
* action was aborted or failed.
|
|
||||||
*
|
|
||||||
* @param {object=} opts parameters for creating the room
|
* @param {object=} opts parameters for creating the room
|
||||||
* @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them
|
* @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them
|
||||||
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
||||||
|
*
|
||||||
|
* @returns {Promise} which resolves to the room id, or null if the
|
||||||
|
* action was aborted or failed.
|
||||||
*/
|
*/
|
||||||
function createRoom(opts) {
|
function createRoom(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -69,16 +69,22 @@ function createRoom(opts) {
|
||||||
createOpts.initial_state = createOpts.initial_state || [
|
createOpts.initial_state = createOpts.initial_state || [
|
||||||
{
|
{
|
||||||
content: {
|
content: {
|
||||||
guest_access: 'can_join'
|
guest_access: 'can_join',
|
||||||
},
|
},
|
||||||
type: 'm.room.guest_access',
|
type: 'm.room.guest_access',
|
||||||
state_key: '',
|
state_key: '',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
let roomId;
|
let roomId;
|
||||||
|
if (opts.andView) {
|
||||||
|
// We will possibly have a successful join, indicate as such
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'will_join',
|
||||||
|
});
|
||||||
|
}
|
||||||
return client.createRoom(createOpts).finally(function() {
|
return client.createRoom(createOpts).finally(function() {
|
||||||
modal.close();
|
modal.close();
|
||||||
}).then(function(res) {
|
}).then(function(res) {
|
||||||
|
@ -98,10 +104,16 @@ function createRoom(opts) {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
should_peek: false,
|
should_peek: false,
|
||||||
|
// Creating a room will have joined us to the room
|
||||||
|
joined: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return roomId;
|
return roomId;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
// We also failed to join the room (this sets joining to false in RoomViewStore)
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'join_room_error',
|
||||||
|
});
|
||||||
console.error("Failed to create room " + roomId + " " + err);
|
console.error("Failed to create room " + roomId + " " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Failure to create room"),
|
title: _t("Failure to create room"),
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
|
const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
looksValid: function(email) {
|
looksValid: function(email) {
|
||||||
return EMAIL_ADDRESS_REGEX.test(email);
|
return EMAIL_ADDRESS_REGEX.test(email);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = function(dest, src) {
|
module.exports = function(dest, src) {
|
||||||
for (var i in src) {
|
for (const i in src) {
|
||||||
if (src.hasOwnProperty(i)) {
|
if (src.hasOwnProperty(i)) {
|
||||||
dest[i] = src[i];
|
dest[i] = src[i];
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"Add a widget": "Add a widget",
|
||||||
"af": "Afrikaans",
|
"af": "Afrikaans",
|
||||||
"ar-ae": "Arabic (U.A.E.)",
|
"ar-ae": "Arabic (U.A.E.)",
|
||||||
"ar-bh": "Arabic (Bahrain)",
|
"ar-bh": "Arabic (Bahrain)",
|
||||||
|
@ -119,6 +120,8 @@
|
||||||
"zh-sg": "Chinese (Singapore)",
|
"zh-sg": "Chinese (Singapore)",
|
||||||
"zh-tw": "Chinese (Taiwan)",
|
"zh-tw": "Chinese (Taiwan)",
|
||||||
"zu": "Zulu",
|
"zu": "Zulu",
|
||||||
|
"AM": "AM",
|
||||||
|
"PM": "PM",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
||||||
"accept": "accept",
|
"accept": "accept",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
||||||
|
@ -311,6 +314,7 @@
|
||||||
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
||||||
"had": "had",
|
"had": "had",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
|
"Hide Apps": "Hide Apps",
|
||||||
"Hide read receipts": "Hide read receipts",
|
"Hide read receipts": "Hide read receipts",
|
||||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
|
@ -362,6 +366,7 @@
|
||||||
"Markdown is disabled": "Markdown is disabled",
|
"Markdown is disabled": "Markdown is disabled",
|
||||||
"Markdown is enabled": "Markdown is enabled",
|
"Markdown is enabled": "Markdown is enabled",
|
||||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||||
|
"Matrix Apps": "Matrix Apps",
|
||||||
"Members only": "Members only",
|
"Members only": "Members only",
|
||||||
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
||||||
"Missing room_id in request": "Missing room_id in request",
|
"Missing room_id in request": "Missing room_id in request",
|
||||||
|
@ -464,6 +469,7 @@
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
||||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
|
"Show Apps": "Show Apps",
|
||||||
"Show panel": "Show panel",
|
"Show panel": "Show panel",
|
||||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||||
"Signed Out": "Signed Out",
|
"Signed Out": "Signed Out",
|
||||||
|
|
|
@ -50,7 +50,7 @@ class LifecycleStore extends Store {
|
||||||
deferred_action: null,
|
deferred_action: null,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'sync_state':
|
case 'sync_state': {
|
||||||
if (payload.state !== 'PREPARED') {
|
if (payload.state !== 'PREPARED') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@ class LifecycleStore extends Store {
|
||||||
});
|
});
|
||||||
dis.dispatch(deferredAction);
|
dis.dispatch(deferredAction);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'on_logged_out':
|
case 'on_logged_out':
|
||||||
this.reset();
|
this.reset();
|
||||||
break;
|
break;
|
||||||
|
|
77
src/stores/MessageComposerStore.js
Normal file
77
src/stores/MessageComposerStore.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import dis from '../dispatcher';
|
||||||
|
import {Store} from 'flux/utils';
|
||||||
|
import {convertToRaw, convertFromRaw} from 'draft-js';
|
||||||
|
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
editorStateMap: localStorage.getItem('content_state') ?
|
||||||
|
JSON.parse(localStorage.getItem('content_state')) : {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for storing application state to do with the message composer. This is a simple
|
||||||
|
* flux store that listens for actions and updates its state accordingly, informing any
|
||||||
|
* listeners (views) of state changes.
|
||||||
|
*/
|
||||||
|
class MessageComposerStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(dis);
|
||||||
|
|
||||||
|
// Initialise state
|
||||||
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setState(newState) {
|
||||||
|
this._state = Object.assign(this._state, newState);
|
||||||
|
this.__emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
__onDispatch(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'content_state':
|
||||||
|
this._contentState(payload);
|
||||||
|
break;
|
||||||
|
case 'on_logged_out':
|
||||||
|
this.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_contentState(payload) {
|
||||||
|
const editorStateMap = this._state.editorStateMap;
|
||||||
|
editorStateMap[payload.room_id] = convertToRaw(payload.content_state);
|
||||||
|
localStorage.setItem('content_state', JSON.stringify(editorStateMap));
|
||||||
|
this._setState({
|
||||||
|
editorStateMap: editorStateMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentState(roomId) {
|
||||||
|
return this._state.editorStateMap[roomId] ?
|
||||||
|
convertFromRaw(this._state.editorStateMap[roomId]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let singletonMessageComposerStore = null;
|
||||||
|
if (!singletonMessageComposerStore) {
|
||||||
|
singletonMessageComposerStore = new MessageComposerStore();
|
||||||
|
}
|
||||||
|
module.exports = singletonMessageComposerStore;
|
|
@ -141,6 +141,10 @@ class RoomViewStore extends Store {
|
||||||
shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
|
shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (payload.joined) {
|
||||||
|
newState.joining = false;
|
||||||
|
}
|
||||||
|
|
||||||
// If an event ID wasn't specified, default to the one saved for this room
|
// If an event ID wasn't specified, default to the one saved for this room
|
||||||
// via update_scroll_state. Assume initialEventPixelOffset should be set.
|
// via update_scroll_state. Assume initialEventPixelOffset should be set.
|
||||||
if (!newState.initialEventId) {
|
if (!newState.initialEventId) {
|
||||||
|
|
1
src/stripped-emoji.json
Normal file
1
src/stripped-emoji.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -192,52 +192,37 @@ describe('ScrollPanel', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle scrollEvent strangeness', function(done) {
|
it('should handle scrollEvent strangeness', function() {
|
||||||
var events = [];
|
const events = [];
|
||||||
|
|
||||||
q().then(() => {
|
return q().then(() => {
|
||||||
// initialise with a few events
|
// initialise with a load of events
|
||||||
for (var i = 0; i < 10; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
events.push(i+90);
|
events.push(i+80);
|
||||||
}
|
}
|
||||||
tester.setTileKeys(events);
|
tester.setTileKeys(events);
|
||||||
expect(tester.fillCounts.b).toEqual(1);
|
expect(scrollingDiv.scrollHeight).toEqual(3050); // 20*150 + 50
|
||||||
expect(tester.fillCounts.f).toEqual(2);
|
expect(scrollingDiv.scrollTop).toEqual(3050 - 600);
|
||||||
expect(scrollingDiv.scrollHeight).toEqual(1550) // 10*150 + 50
|
|
||||||
expect(scrollingDiv.scrollTop).toEqual(1550 - 600);
|
|
||||||
return tester.awaitScroll();
|
return tester.awaitScroll();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(tester.lastScrollEvent).toBe(950);
|
expect(tester.lastScrollEvent).toBe(3050 - 600);
|
||||||
|
|
||||||
// we want to simulate back-filling as we scroll up
|
tester.scrollPanel().scrollToToken("92", 0);
|
||||||
tester.addFillHandler('b', function() {
|
|
||||||
var newEvents = [];
|
|
||||||
for (var i = 0; i < 10; i++) {
|
|
||||||
newEvents.push(i+80);
|
|
||||||
}
|
|
||||||
events.unshift.apply(events, newEvents);
|
|
||||||
tester.setTileKeys(events);
|
|
||||||
return q(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// simulate scrolling up; this should trigger the backfill
|
|
||||||
scrollingDiv.scrollTop = 200;
|
|
||||||
|
|
||||||
return tester.awaitFill('b');
|
|
||||||
}).then(() => {
|
|
||||||
console.log('filled');
|
|
||||||
|
|
||||||
// at this point, ScrollPanel will have updated scrollTop, but
|
// at this point, ScrollPanel will have updated scrollTop, but
|
||||||
// the event hasn't fired. Stamp over the scrollTop.
|
// the event hasn't fired.
|
||||||
expect(tester.lastScrollEvent).toEqual(200);
|
expect(tester.lastScrollEvent).toEqual(3050 - 600);
|
||||||
expect(scrollingDiv.scrollTop).toEqual(10*150 + 200);
|
expect(scrollingDiv.scrollTop).toEqual(1950);
|
||||||
|
|
||||||
|
// now stamp over the scrollTop.
|
||||||
|
console.log('faking #528');
|
||||||
scrollingDiv.scrollTop = 500;
|
scrollingDiv.scrollTop = 500;
|
||||||
|
|
||||||
return tester.awaitScroll();
|
return tester.awaitScroll();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(tester.lastScrollEvent).toBe(10*150 + 200);
|
expect(tester.lastScrollEvent).toBe(1950);
|
||||||
expect(scrollingDiv.scrollTop).toEqual(10*150 + 200);
|
expect(scrollingDiv.scrollTop).toEqual(1950);
|
||||||
}).done(done);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not get stuck in #528 workaround', function(done) {
|
it('should not get stuck in #528 workaround', function(done) {
|
||||||
|
@ -250,7 +235,7 @@ describe('ScrollPanel', function() {
|
||||||
tester.setTileKeys(events);
|
tester.setTileKeys(events);
|
||||||
expect(tester.fillCounts.b).toEqual(1);
|
expect(tester.fillCounts.b).toEqual(1);
|
||||||
expect(tester.fillCounts.f).toEqual(2);
|
expect(tester.fillCounts.f).toEqual(2);
|
||||||
expect(scrollingDiv.scrollHeight).toEqual(6050) // 40*150 + 50
|
expect(scrollingDiv.scrollHeight).toEqual(6050); // 40*150 + 50
|
||||||
expect(scrollingDiv.scrollTop).toEqual(6050 - 600);
|
expect(scrollingDiv.scrollTop).toEqual(6050 - 600);
|
||||||
|
|
||||||
// try to scroll up, to a non-integer offset.
|
// try to scroll up, to a non-integer offset.
|
||||||
|
|
|
@ -99,7 +99,7 @@ describe('MessageComposerInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
|
it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
|
@ -109,8 +109,8 @@ describe('MessageComposerInput', () => {
|
||||||
expect(spy.args[0][1]).toEqual('a');
|
expect(spy.args[0][1]).toEqual('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages in rich text', () => {
|
it('should send emoji messages when rich text is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
@ -118,7 +118,7 @@ describe('MessageComposerInput', () => {
|
||||||
expect(spy.calledOnce).toEqual(true, 'should send message');
|
expect(spy.calledOnce).toEqual(true, 'should send message');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages in Markdown', () => {
|
it('should send emoji messages when Markdown is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue