Merge remote-tracking branch 'origin/develop' into dbkr/my_groups
						commit
						45986306cc
					
				| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
 | 
			
		||||
 | 
			
		||||
src/AddThreepid.js
 | 
			
		||||
src/async-components/views/dialogs/EncryptedEventDialog.js
 | 
			
		||||
src/autocomplete/AutocompleteProvider.js
 | 
			
		||||
src/autocomplete/Autocompleter.js
 | 
			
		||||
| 
						 | 
				
			
			@ -9,8 +8,6 @@ src/autocomplete/DuckDuckGoProvider.js
 | 
			
		|||
src/autocomplete/EmojiProvider.js
 | 
			
		||||
src/autocomplete/RoomProvider.js
 | 
			
		||||
src/autocomplete/UserProvider.js
 | 
			
		||||
src/Avatar.js
 | 
			
		||||
src/BasePlatform.js
 | 
			
		||||
src/CallHandler.js
 | 
			
		||||
src/component-index.js
 | 
			
		||||
src/components/structures/ContextualMenu.js
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +93,6 @@ src/components/views/rooms/MessageComposerInput.js
 | 
			
		|||
src/components/views/rooms/MessageComposerInputOld.js
 | 
			
		||||
src/components/views/rooms/PresenceLabel.js
 | 
			
		||||
src/components/views/rooms/ReadReceiptMarker.js
 | 
			
		||||
src/components/views/rooms/RoomHeader.js
 | 
			
		||||
src/components/views/rooms/RoomList.js
 | 
			
		||||
src/components/views/rooms/RoomNameEditor.js
 | 
			
		||||
src/components/views/rooms/RoomPreviewBar.js
 | 
			
		||||
| 
						 | 
				
			
			@ -115,16 +111,7 @@ src/components/views/settings/ChangePassword.js
 | 
			
		|||
src/components/views/settings/DevicesPanel.js
 | 
			
		||||
src/components/views/settings/DevicesPanelEntry.js
 | 
			
		||||
src/components/views/settings/EnableNotificationsButton.js
 | 
			
		||||
src/components/views/voip/CallView.js
 | 
			
		||||
src/components/views/voip/IncomingCallBox.js
 | 
			
		||||
src/components/views/voip/VideoFeed.js
 | 
			
		||||
src/components/views/voip/VideoView.js
 | 
			
		||||
src/ContentMessages.js
 | 
			
		||||
src/createRoom.js
 | 
			
		||||
src/DateUtils.js
 | 
			
		||||
src/email.js
 | 
			
		||||
src/Entities.js
 | 
			
		||||
src/extend.js
 | 
			
		||||
src/HtmlUtils.js
 | 
			
		||||
src/ImageUtils.js
 | 
			
		||||
src/Invite.js
 | 
			
		||||
| 
						 | 
				
			
			@ -135,30 +122,20 @@ src/Markdown.js
 | 
			
		|||
src/MatrixClientPeg.js
 | 
			
		||||
src/Modal.js
 | 
			
		||||
src/Notifier.js
 | 
			
		||||
src/ObjectUtils.js
 | 
			
		||||
src/PasswordReset.js
 | 
			
		||||
src/PlatformPeg.js
 | 
			
		||||
src/Presence.js
 | 
			
		||||
src/ratelimitedfunc.js
 | 
			
		||||
src/Resend.js
 | 
			
		||||
src/RichText.js
 | 
			
		||||
src/Roles.js
 | 
			
		||||
src/RoomListSorter.js
 | 
			
		||||
src/RoomNotifs.js
 | 
			
		||||
src/Rooms.js
 | 
			
		||||
src/ScalarAuthClient.js
 | 
			
		||||
src/ScalarMessaging.js
 | 
			
		||||
src/SdkConfig.js
 | 
			
		||||
src/Skinner.js
 | 
			
		||||
src/SlashCommands.js
 | 
			
		||||
src/stores/LifecycleStore.js
 | 
			
		||||
src/TabComplete.js
 | 
			
		||||
src/TabCompleteEntries.js
 | 
			
		||||
src/TextForEvent.js
 | 
			
		||||
src/Tinter.js
 | 
			
		||||
src/UiEffects.js
 | 
			
		||||
src/Unread.js
 | 
			
		||||
src/UserActivity.js
 | 
			
		||||
src/utils/DecryptFile.js
 | 
			
		||||
src/utils/DMRoomMap.js
 | 
			
		||||
src/utils/FormattingUtils.js
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,8 +22,11 @@ git checkout "$curbranch" || git checkout develop
 | 
			
		|||
mkdir node_modules
 | 
			
		||||
npm install
 | 
			
		||||
 | 
			
		||||
(cd node_modules/matrix-js-sdk && npm install)
 | 
			
		||||
# use the version of js-sdk we just used in the react-sdk tests
 | 
			
		||||
rm -r node_modules/matrix-js-sdk
 | 
			
		||||
ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk
 | 
			
		||||
 | 
			
		||||
# ... and, of course, the version of react-sdk we just built
 | 
			
		||||
rm -r node_modules/matrix-react-sdk
 | 
			
		||||
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,15 @@
 | 
			
		|||
# we need trusty for the chrome addon
 | 
			
		||||
dist: trusty
 | 
			
		||||
 | 
			
		||||
# we don't need sudo, so can run in a container, which makes startup much
 | 
			
		||||
# quicker.
 | 
			
		||||
sudo: false
 | 
			
		||||
 | 
			
		||||
language: node_js
 | 
			
		||||
node_js:
 | 
			
		||||
    - node # Latest stable version of nodejs.
 | 
			
		||||
addons:
 | 
			
		||||
    chrome: stable
 | 
			
		||||
install:
 | 
			
		||||
    - npm install
 | 
			
		||||
    - (cd node_modules/matrix-js-sdk && npm install)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,11 +116,25 @@ module.exports = function (config) {
 | 
			
		|||
        browsers: [
 | 
			
		||||
            'Chrome',
 | 
			
		||||
            //'PhantomJS',
 | 
			
		||||
            //'ChromeHeadless',
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
        customLaunchers: {
 | 
			
		||||
            'ChromeHeadless': {
 | 
			
		||||
                base: 'Chrome',
 | 
			
		||||
                flags: [
 | 
			
		||||
                    // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
 | 
			
		||||
                    '--headless',
 | 
			
		||||
                    '--disable-gpu',
 | 
			
		||||
                    // Without a remote debugging port, Google Chrome exits immediately.
 | 
			
		||||
                    '--remote-debugging-port=9222',
 | 
			
		||||
                ],
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Continuous Integration mode
 | 
			
		||||
        // if true, Karma captures browsers, runs the tests and exits
 | 
			
		||||
        singleRun: true,
 | 
			
		||||
        // singleRun: false,
 | 
			
		||||
 | 
			
		||||
        // Concurrency level
 | 
			
		||||
        // how many browser should be started simultaneous
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,8 +41,8 @@
 | 
			
		|||
    "lintall": "eslint src/ test/",
 | 
			
		||||
    "clean": "rimraf lib",
 | 
			
		||||
    "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
 | 
			
		||||
    "test": "karma start $KARMAFLAGS --browsers PhantomJS",
 | 
			
		||||
    "test-multi": "karma start $KARMAFLAGS --single-run=false"
 | 
			
		||||
    "test": "karma start $KARMAFLAGS --single-run=true --browsers ChromeHeadless",
 | 
			
		||||
    "test-multi": "karma start $KARMAFLAGS"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "babel-runtime": "^6.11.6",
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +75,7 @@
 | 
			
		|||
    "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
 | 
			
		||||
    "sanitize-html": "^1.11.1",
 | 
			
		||||
    "text-encoding-utf-8": "^1.0.1",
 | 
			
		||||
    "url": "^0.11.0",
 | 
			
		||||
    "velocity-vector": "vector-im/velocity#059e3b2",
 | 
			
		||||
    "whatwg-fetch": "^1.0.0"
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -106,12 +107,10 @@
 | 
			
		|||
    "karma-cli": "^0.1.2",
 | 
			
		||||
    "karma-junit-reporter": "^0.4.1",
 | 
			
		||||
    "karma-mocha": "^0.2.2",
 | 
			
		||||
    "karma-phantomjs-launcher": "^1.0.0",
 | 
			
		||||
    "karma-sourcemap-loader": "^0.3.7",
 | 
			
		||||
    "karma-webpack": "^1.7.0",
 | 
			
		||||
    "mocha": "^2.4.5",
 | 
			
		||||
    "parallelshell": "^1.2.0",
 | 
			
		||||
    "phantomjs-prebuilt": "^2.1.7",
 | 
			
		||||
    "react-addons-test-utils": "^15.4.0",
 | 
			
		||||
    "require-json": "0.0.1",
 | 
			
		||||
    "rimraf": "^2.4.3",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
#!/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(
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +17,9 @@ const output = Object.keys(EMOJI_DATA).map(
 | 
			
		|||
        }
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var MatrixClientPeg = require("./MatrixClientPeg");
 | 
			
		||||
import MatrixClientPeg from './MatrixClientPeg';
 | 
			
		||||
import { _t } from './languageHandler';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ class AddThreepid {
 | 
			
		|||
            this.sessionId = res.sid;
 | 
			
		||||
            return res;
 | 
			
		||||
        }, function(err) {
 | 
			
		||||
            if (err.errcode == 'M_THREEPID_IN_USE') {
 | 
			
		||||
            if (err.errcode === 'M_THREEPID_IN_USE') {
 | 
			
		||||
                err.message = _t('This email address is already in use');
 | 
			
		||||
            } else if (err.httpStatus) {
 | 
			
		||||
                err.message = err.message + ` (Status ${err.httpStatus})`;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +69,7 @@ class AddThreepid {
 | 
			
		|||
            this.sessionId = res.sid;
 | 
			
		||||
            return res;
 | 
			
		||||
        }, function(err) {
 | 
			
		||||
            if (err.errcode == 'M_THREEPID_IN_USE') {
 | 
			
		||||
            if (err.errcode === 'M_THREEPID_IN_USE') {
 | 
			
		||||
                err.message = _t('This phone number is already in use');
 | 
			
		||||
            } else if (err.httpStatus) {
 | 
			
		||||
                err.message = err.message + ` (Status ${err.httpStatus})`;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,16 +85,15 @@ class AddThreepid {
 | 
			
		|||
     * the request failed.
 | 
			
		||||
     */
 | 
			
		||||
    checkEmailLinkClicked() {
 | 
			
		||||
        var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
 | 
			
		||||
        const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
 | 
			
		||||
        return MatrixClientPeg.get().addThreePid({
 | 
			
		||||
            sid: this.sessionId,
 | 
			
		||||
            client_secret: this.clientSecret,
 | 
			
		||||
            id_server: identityServerDomain
 | 
			
		||||
            id_server: identityServerDomain,
 | 
			
		||||
        }, this.bind).catch(function(err) {
 | 
			
		||||
            if (err.httpStatus === 401) {
 | 
			
		||||
                err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
 | 
			
		||||
            }
 | 
			
		||||
            else if (err.httpStatus) {
 | 
			
		||||
            } else if (err.httpStatus) {
 | 
			
		||||
                err.message += ` (Status ${err.httpStatus})`;
 | 
			
		||||
            }
 | 
			
		||||
            throw err;
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +103,7 @@ class AddThreepid {
 | 
			
		|||
    /**
 | 
			
		||||
     * Takes a phone number verification code as entered by the user and validates
 | 
			
		||||
     * it with the ID server, then if successful, adds the phone number.
 | 
			
		||||
     * @param {string} token phone number verification code as entered by the user
 | 
			
		||||
     * @return {Promise} Resolves if the phone number was added. Rejects with an object
 | 
			
		||||
     * with a "message" property which contains a human-readable message detailing why
 | 
			
		||||
     * the request failed.
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ class AddThreepid {
 | 
			
		|||
            return MatrixClientPeg.get().addThreePid({
 | 
			
		||||
                sid: this.sessionId,
 | 
			
		||||
                client_secret: this.clientSecret,
 | 
			
		||||
                id_server: identityServerDomain
 | 
			
		||||
                id_server: identityServerDomain,
 | 
			
		||||
            }, this.bind);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,18 +15,18 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
 | 
			
		||||
var MatrixClientPeg = require('./MatrixClientPeg');
 | 
			
		||||
import {ContentRepo} from 'matrix-js-sdk';
 | 
			
		||||
import MatrixClientPeg from './MatrixClientPeg';
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    avatarUrlForMember: function(member, width, height, resizeMethod) {
 | 
			
		||||
        var url = member.getAvatarUrl(
 | 
			
		||||
        let url = member.getAvatarUrl(
 | 
			
		||||
            MatrixClientPeg.get().getHomeserverUrl(),
 | 
			
		||||
            Math.floor(width * window.devicePixelRatio),
 | 
			
		||||
            Math.floor(height * window.devicePixelRatio),
 | 
			
		||||
            resizeMethod,
 | 
			
		||||
            false,
 | 
			
		||||
            false
 | 
			
		||||
            false,
 | 
			
		||||
        );
 | 
			
		||||
        if (!url) {
 | 
			
		||||
            // member can be null here currently since on invites, the JS SDK
 | 
			
		||||
| 
						 | 
				
			
			@ -38,11 +38,11 @@ module.exports = {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    avatarUrlForUser: function(user, width, height, resizeMethod) {
 | 
			
		||||
        var url = ContentRepo.getHttpUriForMxc(
 | 
			
		||||
        const url = ContentRepo.getHttpUriForMxc(
 | 
			
		||||
            MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
 | 
			
		||||
            Math.floor(width * window.devicePixelRatio),
 | 
			
		||||
            Math.floor(height * window.devicePixelRatio),
 | 
			
		||||
            resizeMethod
 | 
			
		||||
            resizeMethod,
 | 
			
		||||
        );
 | 
			
		||||
        if (!url || url.length === 0) {
 | 
			
		||||
            return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -51,11 +51,11 @@ module.exports = {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    defaultAvatarUrlForString: function(s) {
 | 
			
		||||
        var images = ['76cfa6', '50e2c2', 'f4c371'];
 | 
			
		||||
        var total = 0;
 | 
			
		||||
        for (var i = 0; i < s.length; ++i) {
 | 
			
		||||
        const images = ['76cfa6', '50e2c2', 'f4c371'];
 | 
			
		||||
        let total = 0;
 | 
			
		||||
        for (let i = 0; i < s.length; ++i) {
 | 
			
		||||
            total += s.charCodeAt(i);
 | 
			
		||||
        }
 | 
			
		||||
        return 'img/' + images[total % images.length] + '.png';
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,6 +57,7 @@ export default class BasePlatform {
 | 
			
		|||
    /**
 | 
			
		||||
     * Returns true if the platform supports displaying
 | 
			
		||||
     * notifications, otherwise false.
 | 
			
		||||
     * @returns {boolean} whether the platform supports displaying notifications
 | 
			
		||||
     */
 | 
			
		||||
    supportsNotifications(): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +66,7 @@ export default class BasePlatform {
 | 
			
		|||
    /**
 | 
			
		||||
     * Returns true if the application currently has permission
 | 
			
		||||
     * to display notifications. Otherwise false.
 | 
			
		||||
     * @returns {boolean} whether the application has permission to display notifications
 | 
			
		||||
     */
 | 
			
		||||
    maySendNotifications(): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,24 +54,25 @@ function pad(n) {
 | 
			
		|||
function twelveHourTime(date) {
 | 
			
		||||
    let hours = date.getHours() % 12;
 | 
			
		||||
    const minutes = pad(date.getMinutes());
 | 
			
		||||
    const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
 | 
			
		||||
    hours = pad(hours ? hours : 12);
 | 
			
		||||
    const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
 | 
			
		||||
    hours = hours ? hours : 12; // convert 0 -> 12
 | 
			
		||||
    return `${hours}:${minutes}${ampm}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    formatDate: function(date, showTwelveHour=false) {
 | 
			
		||||
        var now = new Date();
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const days = getDaysArray();
 | 
			
		||||
        const months = getMonthsArray();
 | 
			
		||||
        if (date.toDateString() === now.toDateString()) {
 | 
			
		||||
            return this.formatTime(date);
 | 
			
		||||
        }
 | 
			
		||||
        else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
 | 
			
		||||
        } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
 | 
			
		||||
            // TODO: use standard date localize function provided in counterpart
 | 
			
		||||
            return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date, showTwelveHour)});
 | 
			
		||||
        }
 | 
			
		||||
        else if (now.getFullYear() === date.getFullYear()) {
 | 
			
		||||
            return _t('%(weekDayName)s %(time)s', {
 | 
			
		||||
                weekDayName: days[date.getDay()],
 | 
			
		||||
                time: this.formatTime(date, showTwelveHour),
 | 
			
		||||
            });
 | 
			
		||||
        } else if (now.getFullYear() === date.getFullYear()) {
 | 
			
		||||
            // TODO: use standard date localize function provided in counterpart
 | 
			
		||||
            return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
 | 
			
		||||
                weekDayName: days[date.getDay()],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var React = require('react');
 | 
			
		||||
var sdk = require('./index');
 | 
			
		||||
import sdk from './index';
 | 
			
		||||
 | 
			
		||||
function isMatch(query, name, uid) {
 | 
			
		||||
    query = query.toLowerCase();
 | 
			
		||||
| 
						 | 
				
			
			@ -33,8 +32,8 @@ function isMatch(query, name, uid) {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // split spaces in name and try matching constituent parts
 | 
			
		||||
    var parts = name.split(" ");
 | 
			
		||||
    for (var i = 0; i < parts.length; i++) {
 | 
			
		||||
    const parts = name.split(" ");
 | 
			
		||||
    for (let i = 0; i < parts.length; i++) {
 | 
			
		||||
        if (parts[i].indexOf(query) === 0) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +66,7 @@ class Entity {
 | 
			
		|||
 | 
			
		||||
class MemberEntity extends Entity {
 | 
			
		||||
    getJsx() {
 | 
			
		||||
        var MemberTile = sdk.getComponent("rooms.MemberTile");
 | 
			
		||||
        const MemberTile = sdk.getComponent("rooms.MemberTile");
 | 
			
		||||
        return (
 | 
			
		||||
            <MemberTile key={this.model.userId} member={this.model} />
 | 
			
		||||
        );
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +83,7 @@ class UserEntity extends Entity {
 | 
			
		|||
        super(model);
 | 
			
		||||
        this.showInviteButton = Boolean(showInviteButton);
 | 
			
		||||
        this.inviteFn = inviteFn;
 | 
			
		||||
        this.onClick = this.onClick.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onClick() {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,15 +93,15 @@ class UserEntity extends Entity {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getJsx() {
 | 
			
		||||
        var UserTile = sdk.getComponent("rooms.UserTile");
 | 
			
		||||
        const UserTile = sdk.getComponent("rooms.UserTile");
 | 
			
		||||
        return (
 | 
			
		||||
            <UserTile key={this.model.userId} user={this.model}
 | 
			
		||||
                showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
 | 
			
		||||
                showInviteButton={this.showInviteButton} onClick={this.onClick} />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    matches(queryString) {
 | 
			
		||||
        var name = this.model.displayName || this.model.userId;
 | 
			
		||||
        const name = this.model.displayName || this.model.userId;
 | 
			
		||||
        return isMatch(queryString, name, this.model.userId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ class UserEntity extends Entity {
 | 
			
		|||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    newEntity: function(jsx, matchFn) {
 | 
			
		||||
        var entity = new Entity();
 | 
			
		||||
        const entity = new Entity();
 | 
			
		||||
        entity.getJsx = function() {
 | 
			
		||||
            return jsx;
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			@ -137,5 +137,5 @@ module.exports = {
 | 
			
		|||
        return users.map(function(u) {
 | 
			
		||||
            return new UserEntity(u, showInviteButton, inviteFn);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -419,6 +419,8 @@ export function logout() {
 | 
			
		|||
 * listen for events while a session is logged in.
 | 
			
		||||
 */
 | 
			
		||||
function startMatrixClient() {
 | 
			
		||||
    console.log(`Lifecycle: Starting MatrixClient`);
 | 
			
		||||
 | 
			
		||||
    // dispatch this before starting the matrix client: it's used
 | 
			
		||||
    // to add listeners for the 'sync' event so otherwise we'd have
 | 
			
		||||
    // a race condition (and we need to dispatch synchronously for this
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ limitations under the License.
 | 
			
		|||
import commonmark from 'commonmark';
 | 
			
		||||
import escape from 'lodash/escape';
 | 
			
		||||
 | 
			
		||||
const ALLOWED_HTML_TAGS = ['del'];
 | 
			
		||||
const ALLOWED_HTML_TAGS = ['del', 'u'];
 | 
			
		||||
 | 
			
		||||
// These types of node are definitely text
 | 
			
		||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,22 +77,26 @@ class MatrixClientPeg {
 | 
			
		|||
        this._createClient(creds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    start() {
 | 
			
		||||
    async start() {
 | 
			
		||||
        const opts = utils.deepCopy(this.opts);
 | 
			
		||||
        // the react sdk doesn't work without this, so don't allow
 | 
			
		||||
        opts.pendingEventOrdering = "detached";
 | 
			
		||||
 | 
			
		||||
        let promise = this.matrixClient.store.startup();
 | 
			
		||||
        // log any errors when starting up the database (if one exists)
 | 
			
		||||
        promise.catch((err) => {
 | 
			
		||||
        try {
 | 
			
		||||
            let promise = this.matrixClient.store.startup();
 | 
			
		||||
            console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
 | 
			
		||||
            await promise;
 | 
			
		||||
        } catch(err) {
 | 
			
		||||
            // log any errors when starting up the database (if one exists)
 | 
			
		||||
            console.error(`Error starting matrixclient store: ${err}`);
 | 
			
		||||
        });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // regardless of errors, start the client. If we did error out, we'll
 | 
			
		||||
        // just end up doing a full initial /sync.
 | 
			
		||||
        promise.finally(() => {
 | 
			
		||||
            this.get().startClient(opts);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log(`MatrixClientPeg: really starting MatrixClient`);
 | 
			
		||||
        this.get().startClient(opts);
 | 
			
		||||
        console.log(`MatrixClientPeg: MatrixClient started`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCredentials(): MatrixClientCreds {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,8 +23,8 @@ limitations under the License.
 | 
			
		|||
 * { key: $KEY, val: $VALUE, place: "add|del" }
 | 
			
		||||
 */
 | 
			
		||||
module.exports.getKeyValueArrayDiffs = function(before, after) {
 | 
			
		||||
    var results = [];
 | 
			
		||||
    var delta = {};
 | 
			
		||||
    const results = [];
 | 
			
		||||
    const delta = {};
 | 
			
		||||
    Object.keys(before).forEach(function(beforeKey) {
 | 
			
		||||
        delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
 | 
			
		||||
        delta[beforeKey]--; // keys present in the past have -ve values
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +46,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
 | 
			
		|||
                    results.push({ place: "del", key: muxedKey, val: beforeVal });
 | 
			
		||||
                });
 | 
			
		||||
                break;
 | 
			
		||||
            case 0: // A mix of added/removed keys
 | 
			
		||||
            case 0: {// A mix of added/removed keys
 | 
			
		||||
                // compare old & new vals
 | 
			
		||||
                var itemDelta = {};
 | 
			
		||||
                const itemDelta = {};
 | 
			
		||||
                before[muxedKey].forEach(function(beforeVal) {
 | 
			
		||||
                    itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
 | 
			
		||||
                    itemDelta[beforeVal]--;
 | 
			
		||||
| 
						 | 
				
			
			@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
 | 
			
		|||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            default:
 | 
			
		||||
                console.error("Calculated key delta of " + delta[muxedKey] +
 | 
			
		||||
                              " - this should never happen!");
 | 
			
		||||
                console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Shallow-compare two objects for equality: each key and value must be
 | 
			
		||||
 * identical
 | 
			
		||||
 * Shallow-compare two objects for equality: each key and value must be identical
 | 
			
		||||
 * @param {Object} objA First object to compare against the second
 | 
			
		||||
 * @param {Object} objB Second object to compare against the first
 | 
			
		||||
 * @return {boolean} whether the two objects have same key=values
 | 
			
		||||
 */
 | 
			
		||||
module.exports.shallowEqual = function(objA, objB) {
 | 
			
		||||
    if (objA === objB) {
 | 
			
		||||
| 
						 | 
				
			
			@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) {
 | 
			
		|||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var keysA = Object.keys(objA);
 | 
			
		||||
    var keysB = Object.keys(objB);
 | 
			
		||||
    const keysA = Object.keys(objA);
 | 
			
		||||
    const keysB = Object.keys(objB);
 | 
			
		||||
 | 
			
		||||
    if (keysA.length !== keysB.length) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < keysA.length; i++) {
 | 
			
		||||
        var key = keysA[i];
 | 
			
		||||
    for (let i = 0; i < keysA.length; i++) {
 | 
			
		||||
        const key = keysA[i];
 | 
			
		||||
        if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var Matrix = require("matrix-js-sdk");
 | 
			
		||||
import * as Matrix from 'matrix-js-sdk';
 | 
			
		||||
import { _t } from './languageHandler';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ class PasswordReset {
 | 
			
		|||
    constructor(homeserverUrl, identityUrl) {
 | 
			
		||||
        this.client = Matrix.createClient({
 | 
			
		||||
            baseUrl: homeserverUrl,
 | 
			
		||||
            idBaseUrl: identityUrl
 | 
			
		||||
            idBaseUrl: identityUrl,
 | 
			
		||||
        });
 | 
			
		||||
        this.clientSecret = this.client.generateClientSecret();
 | 
			
		||||
        this.identityServerDomain = identityUrl.split("://")[1];
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ class PasswordReset {
 | 
			
		|||
            this.sessionId = res.sid;
 | 
			
		||||
            return res;
 | 
			
		||||
        }, function(err) {
 | 
			
		||||
            if (err.errcode == 'M_THREEPID_NOT_FOUND') {
 | 
			
		||||
            if (err.errcode === 'M_THREEPID_NOT_FOUND') {
 | 
			
		||||
                 err.message = _t('This email address was not found');
 | 
			
		||||
            } else if (err.httpStatus) {
 | 
			
		||||
                err.message = err.message + ` (Status ${err.httpStatus})`;
 | 
			
		||||
| 
						 | 
				
			
			@ -75,16 +75,15 @@ class PasswordReset {
 | 
			
		|||
            threepid_creds: {
 | 
			
		||||
                sid: this.sessionId,
 | 
			
		||||
                client_secret: this.clientSecret,
 | 
			
		||||
                id_server: this.identityServerDomain
 | 
			
		||||
            }
 | 
			
		||||
                id_server: this.identityServerDomain,
 | 
			
		||||
            },
 | 
			
		||||
        }, this.password).catch(function(err) {
 | 
			
		||||
            if (err.httpStatus === 401) {
 | 
			
		||||
                err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
 | 
			
		||||
            }
 | 
			
		||||
            else if (err.httpStatus === 404) {
 | 
			
		||||
                err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
 | 
			
		||||
            }
 | 
			
		||||
            else if (err.httpStatus) {
 | 
			
		||||
            } else if (err.httpStatus === 404) {
 | 
			
		||||
                err.message =
 | 
			
		||||
                    _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
 | 
			
		||||
            } else if (err.httpStatus) {
 | 
			
		||||
                err.message += ` (Status ${err.httpStatus})`;
 | 
			
		||||
            }
 | 
			
		||||
            throw err;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var MatrixClientPeg = require('./MatrixClientPeg');
 | 
			
		||||
var dis = require('./dispatcher');
 | 
			
		||||
var sdk = require('./index');
 | 
			
		||||
var Modal = require('./Modal');
 | 
			
		||||
import MatrixClientPeg from './MatrixClientPeg';
 | 
			
		||||
import dis from './dispatcher';
 | 
			
		||||
import { EventStatus } from 'matrix-js-sdk';
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,12 +35,10 @@ module.exports = {
 | 
			
		|||
    },
 | 
			
		||||
    resend: function(event) {
 | 
			
		||||
        const room = MatrixClientPeg.get().getRoom(event.getRoomId());
 | 
			
		||||
        MatrixClientPeg.get().resendEvent(
 | 
			
		||||
            event, room
 | 
			
		||||
        ).done(function(res) {
 | 
			
		||||
        MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'message_sent',
 | 
			
		||||
                event: event
 | 
			
		||||
                event: event,
 | 
			
		||||
            });
 | 
			
		||||
        }, function(err) {
 | 
			
		||||
            // XXX: temporary logging to try to diagnose
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +54,7 @@ module.exports = {
 | 
			
		|||
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'message_send_failed',
 | 
			
		||||
                event: event
 | 
			
		||||
                event: event,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +62,7 @@ module.exports = {
 | 
			
		|||
        MatrixClientPeg.get().cancelPendingEvent(event);
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'message_send_cancelled',
 | 
			
		||||
            event: event
 | 
			
		||||
            event: event,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ export function levelRoleMap() {
 | 
			
		|||
    return {
 | 
			
		||||
        undefined: _t('Default'),
 | 
			
		||||
        0: _t('User'),
 | 
			
		||||
        50:  _t('Moderator'),
 | 
			
		||||
        50: _t('Moderator'),
 | 
			
		||||
        100: _t('Admin'),
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,8 +19,7 @@ limitations under the License.
 | 
			
		|||
function tsOfNewestEvent(room) {
 | 
			
		||||
    if (room.timeline.length) {
 | 
			
		||||
        return room.timeline[room.timeline.length - 1].getTs();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
    } else {
 | 
			
		||||
        return Number.MAX_SAFE_INTEGER;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    mostRecentActivityFirst: mostRecentActivityFirst
 | 
			
		||||
    mostRecentActivityFirst,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function setRoomNotifsState(roomId, newState) {
 | 
			
		||||
    if (newState == MUTE) {
 | 
			
		||||
    if (newState === MUTE) {
 | 
			
		||||
        return setRoomNotifsStateMuted(roomId);
 | 
			
		||||
    } else {
 | 
			
		||||
        return setRoomNotifsStateUnmuted(roomId, newState);
 | 
			
		||||
| 
						 | 
				
			
			@ -80,11 +80,11 @@ function setRoomNotifsStateMuted(roomId) {
 | 
			
		|||
                kind: 'event_match',
 | 
			
		||||
                key: 'room_id',
 | 
			
		||||
                pattern: roomId,
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        actions: [
 | 
			
		||||
            'dont_notify',
 | 
			
		||||
        ]
 | 
			
		||||
        ],
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    return q.all(promises);
 | 
			
		||||
| 
						 | 
				
			
			@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
 | 
			
		|||
        promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (newState == 'all_messages') {
 | 
			
		||||
    if (newState === 'all_messages') {
 | 
			
		||||
        const roomRule = cli.getRoomPushRule('global', roomId);
 | 
			
		||||
        if (roomRule) {
 | 
			
		||||
            promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
 | 
			
		||||
        }
 | 
			
		||||
    } else if (newState == 'mentions_only') {
 | 
			
		||||
    } else if (newState === 'mentions_only') {
 | 
			
		||||
        promises.push(cli.addPushRule('global', 'room', roomId, {
 | 
			
		||||
            actions: [
 | 
			
		||||
                'dont_notify',
 | 
			
		||||
            ]
 | 
			
		||||
            ],
 | 
			
		||||
        }));
 | 
			
		||||
        // https://matrix.org/jira/browse/SPEC-400
 | 
			
		||||
        promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
 | 
			
		||||
| 
						 | 
				
			
			@ -119,8 +119,8 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
 | 
			
		|||
                {
 | 
			
		||||
                    set_tweak: 'sound',
 | 
			
		||||
                    value: 'default',
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        }));
 | 
			
		||||
        // https://matrix.org/jira/browse/SPEC-400
 | 
			
		||||
        promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
 | 
			
		||||
| 
						 | 
				
			
			@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) {
 | 
			
		|||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    const cond = rule.conditions[0];
 | 
			
		||||
    if (
 | 
			
		||||
        cond.kind == 'event_match' &&
 | 
			
		||||
        cond.key == 'room_id' &&
 | 
			
		||||
        cond.pattern == roomId
 | 
			
		||||
    ) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
    return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isMuteRule(rule) {
 | 
			
		||||
    return (
 | 
			
		||||
        rule.actions.length == 1 &&
 | 
			
		||||
        rule.actions[0] == 'dont_notify'
 | 
			
		||||
    );
 | 
			
		||||
    return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var DEFAULTS = {
 | 
			
		||||
const DEFAULTS = {
 | 
			
		||||
    // URL to a page we show in an iframe to configure integrations
 | 
			
		||||
    integrations_ui_url: "https://scalar.vector.im/",
 | 
			
		||||
    // Base URL to the REST interface of the integrations server
 | 
			
		||||
| 
						 | 
				
			
			@ -30,8 +30,8 @@ class SdkConfig {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    static put(cfg) {
 | 
			
		||||
        var defaultKeys = Object.keys(DEFAULTS);
 | 
			
		||||
        for (var i = 0; i < defaultKeys.length; ++i) {
 | 
			
		||||
        const defaultKeys = Object.keys(DEFAULTS);
 | 
			
		||||
        for (let i = 0; i < defaultKeys.length; ++i) {
 | 
			
		||||
            if (cfg[defaultKeys[i]] === undefined) {
 | 
			
		||||
                cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,19 +51,18 @@ class Skinner {
 | 
			
		|||
        if (this.components !== null) {
 | 
			
		||||
            throw new Error(
 | 
			
		||||
                "Attempted to load a skin while a skin is already loaded"+
 | 
			
		||||
                "If you want to change the active skin, call resetSkin first"
 | 
			
		||||
            );
 | 
			
		||||
                "If you want to change the active skin, call resetSkin first");
 | 
			
		||||
        }
 | 
			
		||||
        this.components = {};
 | 
			
		||||
        var compKeys = Object.keys(skinObject.components);
 | 
			
		||||
        for (var i = 0; i < compKeys.length; ++i) {
 | 
			
		||||
            var comp = skinObject.components[compKeys[i]];
 | 
			
		||||
        const compKeys = Object.keys(skinObject.components);
 | 
			
		||||
        for (let i = 0; i < compKeys.length; ++i) {
 | 
			
		||||
            const comp = skinObject.components[compKeys[i]];
 | 
			
		||||
            this.addComponent(compKeys[i], comp);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addComponent(name, comp) {
 | 
			
		||||
        var slot = name;
 | 
			
		||||
        let slot = name;
 | 
			
		||||
        if (comp.replaces !== undefined) {
 | 
			
		||||
            if (comp.replaces.indexOf('.') > -1) {
 | 
			
		||||
                slot = comp.replaces;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -186,7 +186,7 @@ const commands = {
 | 
			
		|||
                    if (targetRoomId) { break; }
 | 
			
		||||
                }
 | 
			
		||||
                if (!targetRoomId) {
 | 
			
		||||
                    return reject(_t("Unrecognised room alias:") +  ' ' + roomAlias);
 | 
			
		||||
                    return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -344,8 +344,7 @@ const commands = {
 | 
			
		|||
                        _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
 | 
			
		||||
                           ' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
 | 
			
		||||
                           ' "%(fingerprint)s". This could mean your communications are being intercepted!',
 | 
			
		||||
                            {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
 | 
			
		||||
                    );
 | 
			
		||||
                            {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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var dis = require("./dispatcher");
 | 
			
		||||
import dis from './dispatcher';
 | 
			
		||||
 | 
			
		||||
var MIN_DISPATCH_INTERVAL_MS = 500;
 | 
			
		||||
var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
 | 
			
		||||
const MIN_DISPATCH_INTERVAL_MS = 500;
 | 
			
		||||
const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class watches for user activity (moving the mouse or pressing a key)
 | 
			
		||||
| 
						 | 
				
			
			@ -58,16 +58,15 @@ class UserActivity {
 | 
			
		|||
    /**
 | 
			
		||||
     * Return true if there has been user activity very recently
 | 
			
		||||
     * (ie. within a few seconds)
 | 
			
		||||
     * @returns {boolean} true if user is currently/very recently active
 | 
			
		||||
     */
 | 
			
		||||
    userCurrentlyActive() {
 | 
			
		||||
        return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onUserActivity(event) {
 | 
			
		||||
        if (event.screenX && event.type == "mousemove") {
 | 
			
		||||
            if (event.screenX === this.lastScreenX &&
 | 
			
		||||
                event.screenY === this.lastScreenY)
 | 
			
		||||
            {
 | 
			
		||||
        if (event.screenX && event.type === "mousemove") {
 | 
			
		||||
            if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
 | 
			
		||||
                // mouse hasn't actually moved
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -79,28 +78,24 @@ class UserActivity {
 | 
			
		|||
        if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
 | 
			
		||||
            this.lastDispatchAtTs = this.lastActivityAtTs;
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'user_activity'
 | 
			
		||||
                action: 'user_activity',
 | 
			
		||||
            });
 | 
			
		||||
            if (!this.activityEndTimer) {
 | 
			
		||||
                this.activityEndTimer = setTimeout(
 | 
			
		||||
                    this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
 | 
			
		||||
                );
 | 
			
		||||
                this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onActivityEndTimer() {
 | 
			
		||||
        var now = new Date().getTime();
 | 
			
		||||
        var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
 | 
			
		||||
        const now = new Date().getTime();
 | 
			
		||||
        const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
 | 
			
		||||
        if (now >= targetTime) {
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'user_activity_end'
 | 
			
		||||
                action: 'user_activity_end',
 | 
			
		||||
            });
 | 
			
		||||
            this.activityEndTimer = undefined;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.activityEndTimer = setTimeout(
 | 
			
		||||
                this._onActivityEndTimer.bind(this), targetTime - now
 | 
			
		||||
            );
 | 
			
		||||
            this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ import AutocompleteProvider from './AutocompleteProvider';
 | 
			
		|||
import FuzzyMatcher from './FuzzyMatcher';
 | 
			
		||||
import {TextualCompletion} from './Components';
 | 
			
		||||
 | 
			
		||||
// TODO merge this with the factory mechanics of SlashCommands?
 | 
			
		||||
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
 | 
			
		||||
const COMMANDS = [
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,11 +29,6 @@ const COMMANDS = [
 | 
			
		|||
        args: '<message>',
 | 
			
		||||
        description: 'Displays action',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/part',
 | 
			
		||||
        args: '[#alias:domain]',
 | 
			
		||||
        description: 'Leave room',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/ban',
 | 
			
		||||
        args: '<user-id> [reason]',
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +39,11 @@ const COMMANDS = [
 | 
			
		|||
        args: '<user-id>',
 | 
			
		||||
        description: 'Unbans user with given id',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/op',
 | 
			
		||||
        args: '<user-id> [<power-level>]',
 | 
			
		||||
        description: 'Define the power level of a user',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/deop',
 | 
			
		||||
        args: '<user-id>',
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +59,16 @@ const COMMANDS = [
 | 
			
		|||
        args: '<room-alias>',
 | 
			
		||||
        description: 'Joins room with given alias',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/part',
 | 
			
		||||
        args: '[<room-alias>]',
 | 
			
		||||
        description: 'Leave room',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/topic',
 | 
			
		||||
        args: '<topic>',
 | 
			
		||||
        description: 'Sets the room topic',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/kick',
 | 
			
		||||
        args: '<user-id> [reason]',
 | 
			
		||||
| 
						 | 
				
			
			@ -74,10 +85,16 @@ const COMMANDS = [
 | 
			
		|||
        description: 'Searches DuckDuckGo for results',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/op',
 | 
			
		||||
        args: '<userId> [<power level>]',
 | 
			
		||||
        description: 'Define the power level of a user',
 | 
			
		||||
        command: '/tint',
 | 
			
		||||
        args: '<color1> [<color2>]',
 | 
			
		||||
        description: 'Changes colour scheme of current room',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        command: '/verify',
 | 
			
		||||
        args: '<user-id> <device-id> <device-signing-key>',
 | 
			
		||||
        description: 'Verifies a user, device, and pubkey tuple',
 | 
			
		||||
    },
 | 
			
		||||
    // Omitting `/markdown` as it only seems to apply to OldComposer
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const COMMAND_RE = /(^\/\w*)/g;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ const CATEGORY_ORDER = [
 | 
			
		|||
];
 | 
			
		||||
 | 
			
		||||
// Match for ":wink:" or ascii-style ";-)" provided by emojione
 | 
			
		||||
const EMOJI_REGEX = new RegExp('(:\\w*:?|' + asciiRegexp + ')', 'g');
 | 
			
		||||
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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ export default class EmojiProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    renderCompletions(completions: [React.Component]): ?React.Component {
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill">
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
 | 
			
		||||
            {completions}
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,6 +69,12 @@ export default class QueryMatcher {
 | 
			
		|||
        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>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -80,13 +86,27 @@ export default class QueryMatcher {
 | 
			
		|||
        if (this.options.shouldMatchWordsOnly) {
 | 
			
		||||
            query = query.replace(/[^\w]/g, '');
 | 
			
		||||
        }
 | 
			
		||||
        const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => {
 | 
			
		||||
        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, '');
 | 
			
		||||
            }
 | 
			
		||||
            return resultKey.indexOf(query) !== -1 ? this.keyMap.objectMap[key] : [];
 | 
			
		||||
        }), (candidate) => this.keyMap.priorityMap.get(candidate)));
 | 
			
		||||
        return results;
 | 
			
		||||
            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 {
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill">
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
 | 
			
		||||
            {completions}
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,10 +37,11 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super(USER_REGEX, {
 | 
			
		||||
            keys: ['name', 'userId'],
 | 
			
		||||
            keys: ['name'],
 | 
			
		||||
        });
 | 
			
		||||
        this.matcher = new FuzzyMatcher([], {
 | 
			
		||||
            keys: ['name', 'userId'],
 | 
			
		||||
            keys: ['name'],
 | 
			
		||||
            shouldMatchPrefix: true,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +51,7 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
        let completions = [];
 | 
			
		||||
        let {command, range} = this.getCurrentCommand(query, selection, force);
 | 
			
		||||
        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 completion = displayName;
 | 
			
		||||
                if (range.start === 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +69,7 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
                    ),
 | 
			
		||||
                    range,
 | 
			
		||||
                };
 | 
			
		||||
            }).slice(0, 4);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return completions;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +91,9 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
            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);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -98,9 +101,10 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
    onUserSpoke(user: RoomMember) {
 | 
			
		||||
        if(user.userId === MatrixClientPeg.get().credentials.userId) return;
 | 
			
		||||
 | 
			
		||||
        // Probably unsafe to compare by reference here?
 | 
			
		||||
        _pull(this.users, user);
 | 
			
		||||
        this.users.splice(0, 0, user);
 | 
			
		||||
        this.users = this.users.splice(
 | 
			
		||||
            this.users.findIndex((user2) => user2.userId === user.userId), 1);
 | 
			
		||||
        this.users = [user, ...this.users];
 | 
			
		||||
 | 
			
		||||
        this.matcher.setObjects(this.users);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +116,7 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    renderCompletions(completions: [React.Component]): ?React.Component {
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill">
 | 
			
		||||
        return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
 | 
			
		||||
            {completions}
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,8 +45,9 @@ export default React.createClass({
 | 
			
		|||
            this.setState({
 | 
			
		||||
                summary: null,
 | 
			
		||||
                error: null,
 | 
			
		||||
            }, () => {
 | 
			
		||||
                this._loadGroupFromServer(newProps.groupId);
 | 
			
		||||
            });
 | 
			
		||||
            this._loadGroupFromServer(newProps.groupId);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,8 +18,12 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
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',
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +40,51 @@ export default React.createClass({
 | 
			
		|||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +121,18 @@ export default React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    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">
 | 
			
		||||
| 
						 | 
				
			
			@ -93,9 +154,7 @@ export default React.createClass({
 | 
			
		|||
                        />
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="mx_AppTileBody">
 | 
			
		||||
                    <iframe ref="appFrame" src={this.props.url} allowFullScreen="true"></iframe>
 | 
			
		||||
                </div>
 | 
			
		||||
                {appTileBody}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,9 +143,15 @@ module.exports = React.createClass({
 | 
			
		|||
        if (this.props.showUrlPreview && !this.state.links.length) {
 | 
			
		||||
            var links = this.findLinks(this.refs.content.children);
 | 
			
		||||
            if (links.length) {
 | 
			
		||||
                this.setState({ links: links.map((link)=>{
 | 
			
		||||
                    return link.getAttribute("href");
 | 
			
		||||
                })});
 | 
			
		||||
                // de-dup the links (but preserve ordering)
 | 
			
		||||
                const seen = new Set();
 | 
			
		||||
                links = links.filter((link) => {
 | 
			
		||||
                    if (seen.has(link)) return false;
 | 
			
		||||
                    seen.add(link);
 | 
			
		||||
                    return true;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.setState({ links: links });
 | 
			
		||||
 | 
			
		||||
                // lazy-load the hidden state of the preview widget from localstorage
 | 
			
		||||
                if (global.localStorage) {
 | 
			
		||||
| 
						 | 
				
			
			@ -158,12 +164,13 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
    findLinks: function(nodes) {
 | 
			
		||||
        var links = [];
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < nodes.length; i++) {
 | 
			
		||||
            var node = nodes[i];
 | 
			
		||||
            if (node.tagName === "A" && node.getAttribute("href"))
 | 
			
		||||
            {
 | 
			
		||||
                if (this.isLinkPreviewable(node)) {
 | 
			
		||||
                    links.push(node);
 | 
			
		||||
                    links.push(node.getAttribute("href"));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (node.tagName === "PRE" || node.tagName === "CODE" ||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ export default class Autocomplete extends React.Component {
 | 
			
		|||
        let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
 | 
			
		||||
 | 
			
		||||
        // Don't debounce if we are already showing completions
 | 
			
		||||
        if (this.state.completions.length > 0) {
 | 
			
		||||
        if (this.state.completions.length > 0 || this.state.forceComplete) {
 | 
			
		||||
            autocompleteDelay = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +177,7 @@ export default class Autocomplete extends React.Component {
 | 
			
		|||
            hide: false,
 | 
			
		||||
        }, () => {
 | 
			
		||||
            this.complete(this.props.query, this.props.selection).then(() => {
 | 
			
		||||
                done.resolve();
 | 
			
		||||
                done.resolve(this.countCompletions());
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        return done.promise;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,6 @@ import Modal from '../../../Modal';
 | 
			
		|||
import sdk from '../../../index';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
import Autocomplete from './Autocomplete';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import UserSettingsStore from '../../../UserSettingsStore';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -408,14 +407,10 @@ export default class MessageComposer extends React.Component {
 | 
			
		|||
                const active = style.includes(name) || blockType === name;
 | 
			
		||||
                const suffix = active ? '-o-n' : '';
 | 
			
		||||
                const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
 | 
			
		||||
                const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
 | 
			
		||||
                const className = classNames("mx_MessageComposer_format_button", {
 | 
			
		||||
                    mx_MessageComposer_format_button_disabled: disabled,
 | 
			
		||||
                    mx_filterFlipColor: true,
 | 
			
		||||
                });
 | 
			
		||||
                const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
 | 
			
		||||
                return <img className={className}
 | 
			
		||||
                            title={ _t(name) }
 | 
			
		||||
                            onMouseDown={disabled ? null : onFormatButtonClicked}
 | 
			
		||||
                            onMouseDown={onFormatButtonClicked}
 | 
			
		||||
                            key={name}
 | 
			
		||||
                            src={`img/button-text-${name}${suffix}.svg`}
 | 
			
		||||
                            height="17" />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,8 @@ import Markdown from '../../../Markdown';
 | 
			
		|||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
 | 
			
		||||
import {onSendMessageFailed} from './MessageComposerInputOld';
 | 
			
		||||
 | 
			
		||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
 | 
			
		||||
 | 
			
		||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
 | 
			
		||||
 | 
			
		||||
const ZWS_CODE = 8203;
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +132,10 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
            isRichtextEnabled,
 | 
			
		||||
 | 
			
		||||
            // 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
 | 
			
		||||
            originalEditorState: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -138,11 +143,10 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
            // the virtual state "above" the history stack, the message currently being composed that
 | 
			
		||||
            // we want to persist whilst browsing history
 | 
			
		||||
            currentlyComposedEditorState: null,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
 | 
			
		||||
        /* eslint react/no-direct-mutation-state:0 */
 | 
			
		||||
        this.state.editorState = this.createEditorState();
 | 
			
		||||
            // whether there were any completions
 | 
			
		||||
            someCompletions: null,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.client = MatrixClientPeg.get();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -336,6 +340,14 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
                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')) {
 | 
			
		||||
                state.originalEditorState = null;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -403,26 +415,59 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            let contentState = this.state.editorState.getCurrentContent(),
 | 
			
		||||
                selection = this.state.editorState.getSelection();
 | 
			
		||||
            let contentState = this.state.editorState.getCurrentContent();
 | 
			
		||||
 | 
			
		||||
            const modifyFn = {
 | 
			
		||||
                'bold': (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>`,
 | 
			
		||||
                'code-block': (text) => `\`\`\`\n${text}\n\`\`\``,
 | 
			
		||||
                'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''),
 | 
			
		||||
                'code-block': (text) => `\`\`\`\n${text}\n\`\`\`\n`,
 | 
			
		||||
                'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join('') + '\n',
 | 
			
		||||
                '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(''),
 | 
			
		||||
            }[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) {
 | 
			
		||||
                const previousSelection = this.state.editorState.getSelection();
 | 
			
		||||
                const newContentState = RichText.modifyText(contentState, previousSelection, modifyFn);
 | 
			
		||||
                newState = EditorState.push(
 | 
			
		||||
                    this.state.editorState,
 | 
			
		||||
                    RichText.modifyText(contentState, selection, modifyFn),
 | 
			
		||||
                    newContentState,
 | 
			
		||||
                    '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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -443,8 +488,7 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
        const currentContent = this.state.editorState.getCurrentContent();
 | 
			
		||||
 | 
			
		||||
        let contentState = null;
 | 
			
		||||
 | 
			
		||||
        if (html) {
 | 
			
		||||
        if (html && this.state.isRichtextEnabled) {
 | 
			
		||||
            contentState = Modifier.replaceWithFragment(
 | 
			
		||||
                currentContent,
 | 
			
		||||
                currentSelection,
 | 
			
		||||
| 
						 | 
				
			
			@ -548,14 +592,6 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
        let sendHtmlFn = this.client.sendHtmlMessage;
 | 
			
		||||
        let sendTextFn = this.client.sendTextMessage;
 | 
			
		||||
 | 
			
		||||
        if (contentText.startsWith('/me')) {
 | 
			
		||||
            contentText = contentText.substring(4);
 | 
			
		||||
            // bit of a hack, but the alternative would be quite complicated
 | 
			
		||||
            if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
 | 
			
		||||
            sendHtmlFn = this.client.sendHtmlEmote;
 | 
			
		||||
            sendTextFn = this.client.sendEmoteMessage;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.state.isRichtextEnabled) {
 | 
			
		||||
            this.historyManager.addItem(
 | 
			
		||||
                contentHTML ? contentHTML : contentText,
 | 
			
		||||
| 
						 | 
				
			
			@ -566,6 +602,14 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
            this.historyManager.addItem(contentText, 'markdown');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (contentText.startsWith('/me')) {
 | 
			
		||||
            contentText = contentText.substring(4);
 | 
			
		||||
            // bit of a hack, but the alternative would be quite complicated
 | 
			
		||||
            if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
 | 
			
		||||
            sendHtmlFn = this.client.sendHtmlEmote;
 | 
			
		||||
            sendTextFn = this.client.sendEmoteMessage;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let sendMessagePromise;
 | 
			
		||||
        if (contentHTML) {
 | 
			
		||||
            sendMessagePromise = sendHtmlFn.call(
 | 
			
		||||
| 
						 | 
				
			
			@ -599,6 +643,10 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    onVerticalArrow = (e, up) => {
 | 
			
		||||
        if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Select history only if we are not currently auto-completing
 | 
			
		||||
        if (this.autocomplete.state.completionList.length === 0) {
 | 
			
		||||
            // Don't go back in history if we're in the middle of a multi-line message
 | 
			
		||||
| 
						 | 
				
			
			@ -607,17 +655,16 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
            const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock();
 | 
			
		||||
            const lastBlock = this.state.editorState.getCurrentContent().getLastBlock();
 | 
			
		||||
 | 
			
		||||
            const selectionOffset = selection.getAnchorOffset();
 | 
			
		||||
            let canMoveUp = false;
 | 
			
		||||
            let canMoveDown = false;
 | 
			
		||||
            if (blockKey === firstBlock.getKey()) {
 | 
			
		||||
                const textBeforeCursor = firstBlock.getText().slice(0, selectionOffset);
 | 
			
		||||
                canMoveUp = textBeforeCursor.indexOf('\n') === -1;
 | 
			
		||||
                canMoveUp = selection.getStartOffset() === selection.getEndOffset() &&
 | 
			
		||||
                    selection.getStartOffset() === 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (blockKey === lastBlock.getKey()) {
 | 
			
		||||
                const textAfterCursor = lastBlock.getText().slice(selectionOffset);
 | 
			
		||||
                canMoveDown = textAfterCursor.indexOf('\n') === -1;
 | 
			
		||||
                canMoveDown = selection.getStartOffset() === selection.getEndOffset() &&
 | 
			
		||||
                    selection.getStartOffset() === lastBlock.getText().length;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -674,10 +721,16 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
            await this.autocomplete.forceComplete();
 | 
			
		||||
            const completionCount = await this.autocomplete.forceComplete();
 | 
			
		||||
            this.setState({
 | 
			
		||||
                someCompletions: completionCount > 0,
 | 
			
		||||
            });
 | 
			
		||||
            // Select the first item by moving "down"
 | 
			
		||||
            await this.moveAutocompleteSelection(false);
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -798,6 +851,7 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
 | 
			
		||||
        const className = classNames('mx_MessageComposer_input', {
 | 
			
		||||
            mx_MessageComposer_input_empty: hidePlaceholder,
 | 
			
		||||
            mx_MessageComposer_input_error: this.state.someCompletions === false,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const content = activeEditorState.getCurrentContent();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,18 +16,18 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var React = require('react');
 | 
			
		||||
var classNames = require('classnames');
 | 
			
		||||
var sdk = require('../../../index');
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import sdk from '../../../index';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
 | 
			
		||||
var Modal = require("../../../Modal");
 | 
			
		||||
var dis = require("../../../dispatcher");
 | 
			
		||||
var rate_limited_func = require('../../../ratelimitedfunc');
 | 
			
		||||
import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		||||
import Modal from "../../../Modal";
 | 
			
		||||
import dis from "../../../dispatcher";
 | 
			
		||||
import RateLimitedFunc from '../../../ratelimitedfunc';
 | 
			
		||||
 | 
			
		||||
var linkify = require('linkifyjs');
 | 
			
		||||
var linkifyElement = require('linkifyjs/element');
 | 
			
		||||
var linkifyMatrix = require('../../../linkify-matrix');
 | 
			
		||||
import * as linkify from 'linkifyjs';
 | 
			
		||||
import linkifyElement from 'linkifyjs/element';
 | 
			
		||||
import linkifyMatrix from '../../../linkify-matrix';
 | 
			
		||||
import AccessibleButton from '../elements/AccessibleButton';
 | 
			
		||||
import {CancelButton} from './SimpleRoomHeader';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    componentDidMount: function() {
 | 
			
		||||
        var cli = MatrixClientPeg.get();
 | 
			
		||||
        const cli = MatrixClientPeg.get();
 | 
			
		||||
        cli.on("RoomState.events", this._onRoomStateEvents);
 | 
			
		||||
 | 
			
		||||
        // When a room name occurs, RoomState.events is fired *before*
 | 
			
		||||
| 
						 | 
				
			
			@ -79,14 +79,14 @@ module.exports = React.createClass({
 | 
			
		|||
        if (this.props.room) {
 | 
			
		||||
            this.props.room.removeListener("Room.name", this._onRoomNameChange);
 | 
			
		||||
        }
 | 
			
		||||
        var cli = MatrixClientPeg.get();
 | 
			
		||||
        const cli = MatrixClientPeg.get();
 | 
			
		||||
        if (cli) {
 | 
			
		||||
            cli.removeListener("RoomState.events", this._onRoomStateEvents);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onRoomStateEvents: function(event, state) {
 | 
			
		||||
        if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
 | 
			
		||||
        if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,8 @@ module.exports = React.createClass({
 | 
			
		|||
        this._rateLimitedUpdate();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _rateLimitedUpdate: new rate_limited_func(function() {
 | 
			
		||||
    _rateLimitedUpdate: new RateLimitedFunc(function() {
 | 
			
		||||
        /* eslint-disable babel/no-invalid-this */
 | 
			
		||||
        this.forceUpdate();
 | 
			
		||||
    }, 500),
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,15 +110,14 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    onAvatarSelected: function(ev) {
 | 
			
		||||
        var self = this;
 | 
			
		||||
        var changeAvatar = this.refs.changeAvatar;
 | 
			
		||||
        const changeAvatar = this.refs.changeAvatar;
 | 
			
		||||
        if (!changeAvatar) {
 | 
			
		||||
            console.error("No ChangeAvatar found to upload image to!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        changeAvatar.onFileSelected(ev).catch(function(err) {
 | 
			
		||||
            var errMsg = (typeof err === "string") ? err : (err.error || "");
 | 
			
		||||
            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
            const errMsg = (typeof err === "string") ? err : (err.error || "");
 | 
			
		||||
            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
            console.error("Failed to set avatar: " + errMsg);
 | 
			
		||||
            Modal.createDialog(ErrorDialog, {
 | 
			
		||||
                title: _t("Error"),
 | 
			
		||||
| 
						 | 
				
			
			@ -133,10 +133,10 @@ module.exports = React.createClass({
 | 
			
		|||
    /**
 | 
			
		||||
     * After editing the settings, get the new name for the room
 | 
			
		||||
     *
 | 
			
		||||
     * Returns undefined if we didn't let the user edit the room name
 | 
			
		||||
     * @return {?string} newName or undefined if we didn't let the user edit the room name
 | 
			
		||||
     */
 | 
			
		||||
    getEditedName: function() {
 | 
			
		||||
        var newName;
 | 
			
		||||
        let newName;
 | 
			
		||||
        if (this.refs.nameEditor) {
 | 
			
		||||
            newName = this.refs.nameEditor.getRoomName();
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -146,10 +146,10 @@ module.exports = React.createClass({
 | 
			
		|||
    /**
 | 
			
		||||
     * After editing the settings, get the new topic for the room
 | 
			
		||||
     *
 | 
			
		||||
     * Returns undefined if we didn't let the user edit the room topic
 | 
			
		||||
     * @return {?string} newTopic or undefined if we didn't let the user edit the room topic
 | 
			
		||||
     */
 | 
			
		||||
    getEditedTopic: function() {
 | 
			
		||||
        var newTopic;
 | 
			
		||||
        let newTopic;
 | 
			
		||||
        if (this.refs.topicEditor) {
 | 
			
		||||
            newTopic = this.refs.topicEditor.getTopic();
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -157,38 +157,31 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
 | 
			
		||||
        var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
 | 
			
		||||
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
			
		||||
        const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
 | 
			
		||||
        const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
 | 
			
		||||
        const TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
			
		||||
        const EmojiText = sdk.getComponent('elements.EmojiText');
 | 
			
		||||
 | 
			
		||||
        var header;
 | 
			
		||||
        var name = null;
 | 
			
		||||
        var searchStatus = null;
 | 
			
		||||
        var topic_el = null;
 | 
			
		||||
        var cancel_button = null;
 | 
			
		||||
        var spinner = null;
 | 
			
		||||
        var save_button = null;
 | 
			
		||||
        var settings_button = null;
 | 
			
		||||
        let name = null;
 | 
			
		||||
        let searchStatus = null;
 | 
			
		||||
        let topicElement = null;
 | 
			
		||||
        let cancelButton = null;
 | 
			
		||||
        let spinner = null;
 | 
			
		||||
        let saveButton = null;
 | 
			
		||||
        let settingsButton = null;
 | 
			
		||||
 | 
			
		||||
        let canSetRoomName;
 | 
			
		||||
        let canSetRoomAvatar;
 | 
			
		||||
        let canSetRoomTopic;
 | 
			
		||||
        if (this.props.editing) {
 | 
			
		||||
 | 
			
		||||
            // calculate permissions.  XXX: this should be done on mount or something
 | 
			
		||||
            var user_id = MatrixClientPeg.get().credentials.userId;
 | 
			
		||||
            const userId = MatrixClientPeg.get().credentials.userId;
 | 
			
		||||
 | 
			
		||||
            var can_set_room_name = this.props.room.currentState.maySendStateEvent(
 | 
			
		||||
                'm.room.name', user_id
 | 
			
		||||
            );
 | 
			
		||||
            var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
 | 
			
		||||
                'm.room.avatar', user_id
 | 
			
		||||
            );
 | 
			
		||||
            var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
 | 
			
		||||
                'm.room.topic', user_id
 | 
			
		||||
            );
 | 
			
		||||
            var can_set_room_name = this.props.room.currentState.maySendStateEvent(
 | 
			
		||||
                'm.room.name', user_id
 | 
			
		||||
            );
 | 
			
		||||
            canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
 | 
			
		||||
            canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
 | 
			
		||||
            canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
 | 
			
		||||
 | 
			
		||||
            save_button = (
 | 
			
		||||
            saveButton = (
 | 
			
		||||
                <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
 | 
			
		||||
                    {_t("Save")}
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -196,39 +189,41 @@ module.exports = React.createClass({
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.props.onCancelClick) {
 | 
			
		||||
            cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
 | 
			
		||||
            cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.props.saving) {
 | 
			
		||||
            var Spinner = sdk.getComponent("elements.Spinner");
 | 
			
		||||
            const Spinner = sdk.getComponent("elements.Spinner");
 | 
			
		||||
            spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (can_set_room_name) {
 | 
			
		||||
            var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
 | 
			
		||||
        if (canSetRoomName) {
 | 
			
		||||
            const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
 | 
			
		||||
            name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            var searchStatus;
 | 
			
		||||
        } else {
 | 
			
		||||
            // don't display the search count until the search completes and
 | 
			
		||||
            // gives us a valid (possibly zero) searchCount.
 | 
			
		||||
            if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
 | 
			
		||||
                searchStatus = <div className="mx_RoomHeader_searchStatus"> { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
 | 
			
		||||
            if (this.props.searchInfo &&
 | 
			
		||||
                this.props.searchInfo.searchCount !== undefined &&
 | 
			
		||||
                this.props.searchInfo.searchCount !== null) {
 | 
			
		||||
                searchStatus = <div className="mx_RoomHeader_searchStatus"> 
 | 
			
		||||
                    { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
 | 
			
		||||
                </div>;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
 | 
			
		||||
            var settingsHint = false;
 | 
			
		||||
            var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
 | 
			
		||||
            let settingsHint = false;
 | 
			
		||||
            const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
 | 
			
		||||
            if (members) {
 | 
			
		||||
                if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
 | 
			
		||||
                    var name = this.props.room.currentState.getStateEvents('m.room.name', '');
 | 
			
		||||
                    if (!name || !name.getContent().name) {
 | 
			
		||||
                    const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
 | 
			
		||||
                    if (!nameEvent || !nameEvent.getContent().name) {
 | 
			
		||||
                        settingsHint = true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var roomName = _t("Join Room");
 | 
			
		||||
            let roomName = _t("Join Room");
 | 
			
		||||
            if (this.props.oobData && this.props.oobData.name) {
 | 
			
		||||
                roomName = this.props.oobData.name;
 | 
			
		||||
            } else if (this.props.room) {
 | 
			
		||||
| 
						 | 
				
			
			@ -243,24 +238,25 @@ module.exports = React.createClass({
 | 
			
		|||
                </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (can_set_room_topic) {
 | 
			
		||||
            var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
 | 
			
		||||
            topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
 | 
			
		||||
        if (canSetRoomTopic) {
 | 
			
		||||
            const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
 | 
			
		||||
            topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
 | 
			
		||||
        } else {
 | 
			
		||||
            var topic;
 | 
			
		||||
            let topic;
 | 
			
		||||
            if (this.props.room) {
 | 
			
		||||
                var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
 | 
			
		||||
                const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
 | 
			
		||||
                if (ev) {
 | 
			
		||||
                    topic = ev.getContent().topic;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (topic) {
 | 
			
		||||
                topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
 | 
			
		||||
                topicElement =
 | 
			
		||||
                    <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var roomAvatar = null;
 | 
			
		||||
        if (can_set_room_avatar) {
 | 
			
		||||
        let roomAvatar = null;
 | 
			
		||||
        if (canSetRoomAvatar) {
 | 
			
		||||
            roomAvatar = (
 | 
			
		||||
                <div className="mx_RoomHeader_avatarPicker">
 | 
			
		||||
                    <div onClick={ this.onAvatarPickerClick }>
 | 
			
		||||
| 
						 | 
				
			
			@ -276,8 +272,7 @@ module.exports = React.createClass({
 | 
			
		|||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
 | 
			
		||||
        } else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
 | 
			
		||||
            roomAvatar = (
 | 
			
		||||
                <div onClick={this.props.onSettingsClick}>
 | 
			
		||||
                    <RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
 | 
			
		||||
| 
						 | 
				
			
			@ -285,9 +280,8 @@ module.exports = React.createClass({
 | 
			
		|||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var settings_button;
 | 
			
		||||
        if (this.props.onSettingsClick) {
 | 
			
		||||
            settings_button =
 | 
			
		||||
            settingsButton =
 | 
			
		||||
                <AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
 | 
			
		||||
                    <TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
 | 
			
		||||
                </AccessibleButton>;
 | 
			
		||||
| 
						 | 
				
			
			@ -301,61 +295,58 @@ module.exports = React.createClass({
 | 
			
		|||
//                </div>;
 | 
			
		||||
//        }
 | 
			
		||||
 | 
			
		||||
        var forget_button;
 | 
			
		||||
        let forgetButton;
 | 
			
		||||
        if (this.props.onForgetClick) {
 | 
			
		||||
            forget_button =
 | 
			
		||||
            forgetButton =
 | 
			
		||||
                <AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
 | 
			
		||||
                    <TintableSvg src="img/leave.svg" width="26" height="20"/>
 | 
			
		||||
                </AccessibleButton>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let search_button;
 | 
			
		||||
        let searchButton;
 | 
			
		||||
        if (this.props.onSearchClick && this.props.inRoom) {
 | 
			
		||||
            search_button =
 | 
			
		||||
            searchButton =
 | 
			
		||||
                <AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
 | 
			
		||||
                    <TintableSvg src="img/icons-search.svg" width="35" height="35"/>
 | 
			
		||||
                </AccessibleButton>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var rightPanel_buttons;
 | 
			
		||||
        let rightPanelButtons;
 | 
			
		||||
        if (this.props.collapsedRhs) {
 | 
			
		||||
            rightPanel_buttons =
 | 
			
		||||
            rightPanelButtons =
 | 
			
		||||
                <AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
 | 
			
		||||
                    <TintableSvg src="img/maximise.svg" width="10" height="16"/>
 | 
			
		||||
                </AccessibleButton>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var right_row;
 | 
			
		||||
        let rightRow;
 | 
			
		||||
        if (!this.props.editing) {
 | 
			
		||||
            right_row =
 | 
			
		||||
            rightRow =
 | 
			
		||||
                <div className="mx_RoomHeader_rightRow">
 | 
			
		||||
                    { settings_button }
 | 
			
		||||
                    { forget_button }
 | 
			
		||||
                    { search_button }
 | 
			
		||||
                    { rightPanel_buttons }
 | 
			
		||||
                    { settingsButton }
 | 
			
		||||
                    { forgetButton }
 | 
			
		||||
                    { searchButton }
 | 
			
		||||
                    { rightPanelButtons }
 | 
			
		||||
                </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        header =
 | 
			
		||||
            <div className="mx_RoomHeader_wrapper">
 | 
			
		||||
                <div className="mx_RoomHeader_leftRow">
 | 
			
		||||
                    <div className="mx_RoomHeader_avatar">
 | 
			
		||||
                        { roomAvatar }
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="mx_RoomHeader_info">
 | 
			
		||||
                        { name }
 | 
			
		||||
                        { topic_el }
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {spinner}
 | 
			
		||||
                {save_button}
 | 
			
		||||
                {cancel_button}
 | 
			
		||||
                {right_row}
 | 
			
		||||
            </div>;
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
 | 
			
		||||
                { header }
 | 
			
		||||
                <div className="mx_RoomHeader_wrapper">
 | 
			
		||||
                    <div className="mx_RoomHeader_leftRow">
 | 
			
		||||
                        <div className="mx_RoomHeader_avatar">
 | 
			
		||||
                            { roomAvatar }
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div className="mx_RoomHeader_info">
 | 
			
		||||
                            { name }
 | 
			
		||||
                            { topicElement }
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {spinner}
 | 
			
		||||
                    {saveButton}
 | 
			
		||||
                    {cancelButton}
 | 
			
		||||
                    {rightRow}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) {
 | 
			
		|||
 | 
			
		||||
const BannedUser = React.createClass({
 | 
			
		||||
    propTypes: {
 | 
			
		||||
        canUnban: React.PropTypes.bool,
 | 
			
		||||
        member: React.PropTypes.object.isRequired, // js-sdk RoomMember
 | 
			
		||||
        reason: React.PropTypes.string,
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -67,13 +68,17 @@ const BannedUser = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        let unbanButton;
 | 
			
		||||
 | 
			
		||||
        if (this.props.canUnban) {
 | 
			
		||||
            unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
 | 
			
		||||
                { _t('Unban') }
 | 
			
		||||
            </AccessibleButton>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <li>
 | 
			
		||||
                <AccessibleButton className="mx_RoomSettings_unbanButton"
 | 
			
		||||
                    onClick={this._onUnbanClick}
 | 
			
		||||
                >
 | 
			
		||||
                    { _t('Unban') }
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
                { unbanButton }
 | 
			
		||||
                <strong>{this.props.member.name}</strong> {this.props.member.userId}
 | 
			
		||||
                {this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
 | 
			
		||||
            </li>
 | 
			
		||||
| 
						 | 
				
			
			@ -667,6 +672,7 @@ module.exports = React.createClass({
 | 
			
		|||
        const banned = this.props.room.getMembersWithMembership("ban");
 | 
			
		||||
        let bannedUsersSection;
 | 
			
		||||
        if (banned.length) {
 | 
			
		||||
            const canBanUsers = current_user_level >= ban_level;
 | 
			
		||||
            bannedUsersSection =
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h3>{ _t('Banned users') }</h3>
 | 
			
		||||
| 
						 | 
				
			
			@ -674,7 +680,7 @@ module.exports = React.createClass({
 | 
			
		|||
                        {banned.map(function(member) {
 | 
			
		||||
                            const banEvent = member.events.member.getContent();
 | 
			
		||||
                            return (
 | 
			
		||||
                                <BannedUser key={member.userId} member={member} reason={banEvent.reason} />
 | 
			
		||||
                                <BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
 | 
			
		||||
                            );
 | 
			
		||||
                        })}
 | 
			
		||||
                    </ul>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
var React = require("react");
 | 
			
		||||
var dis = require("../../../dispatcher");
 | 
			
		||||
var CallHandler = require("../../../CallHandler");
 | 
			
		||||
var sdk = require('../../../index');
 | 
			
		||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
import CallHandler from '../../../CallHandler';
 | 
			
		||||
import sdk from '../../../index';
 | 
			
		||||
import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
 | 
			
		||||
module.exports = React.createClass({
 | 
			
		||||
| 
						 | 
				
			
			@ -73,10 +73,10 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    showCall: function() {
 | 
			
		||||
        var call;
 | 
			
		||||
        let call;
 | 
			
		||||
 | 
			
		||||
        if (this.props.room) {
 | 
			
		||||
            var roomId = this.props.room.roomId;
 | 
			
		||||
            const roomId = this.props.room.roomId;
 | 
			
		||||
            call = CallHandler.getCallForRoom(roomId) ||
 | 
			
		||||
                (this.props.ConferenceHandler ?
 | 
			
		||||
                 this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
 | 
			
		||||
| 
						 | 
				
			
			@ -86,9 +86,7 @@ module.exports = React.createClass({
 | 
			
		|||
            if (this.call) {
 | 
			
		||||
                this.setState({ call: call });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
        } else {
 | 
			
		||||
            call = CallHandler.getAnyActiveCall();
 | 
			
		||||
            this.setState({ call: call });
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -109,8 +107,7 @@ module.exports = React.createClass({
 | 
			
		|||
                call.confUserId ? "none" : "block"
 | 
			
		||||
            );
 | 
			
		||||
            this.getVideoView().getRemoteVideoElement().style.display = "block";
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
        } else {
 | 
			
		||||
            this.getVideoView().getLocalVideoElement().style.display = "none";
 | 
			
		||||
            this.getVideoView().getRemoteVideoElement().style.display = "none";
 | 
			
		||||
            dis.dispatch({action: 'video_fullscreen', fullscreen: false});
 | 
			
		||||
| 
						 | 
				
			
			@ -126,11 +123,11 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
            var callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
 | 
			
		||||
            const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
 | 
			
		||||
            voice = (
 | 
			
		||||
                <div className="mx_CallView_voice" onClick={ this.props.onClick }>
 | 
			
		||||
                {_t("Active call (%(roomName)s)", {roomName: callRoom.name})}
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +144,6 @@ module.exports = React.createClass({
 | 
			
		|||
                { voice }
 | 
			
		||||
            </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
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
var React = require('react');
 | 
			
		||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
 | 
			
		||||
var dis = require("../../../dispatcher");
 | 
			
		||||
var CallHandler = require("../../../CallHandler");
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
 | 
			
		||||
module.exports = React.createClass({
 | 
			
		||||
| 
						 | 
				
			
			@ -29,34 +28,32 @@ module.exports = React.createClass({
 | 
			
		|||
    onAnswerClick: function() {
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'answer',
 | 
			
		||||
            room_id: this.props.incomingCall.roomId
 | 
			
		||||
            room_id: this.props.incomingCall.roomId,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    onRejectClick: function() {
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'hangup',
 | 
			
		||||
            room_id: this.props.incomingCall.roomId
 | 
			
		||||
            room_id: this.props.incomingCall.roomId,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        var room = null;
 | 
			
		||||
        let room = null;
 | 
			
		||||
        if (this.props.incomingCall) {
 | 
			
		||||
            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;
 | 
			
		||||
        if (this.props.incomingCall) {
 | 
			
		||||
            if (this.props.incomingCall.type === "voice") {
 | 
			
		||||
                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});
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
            } else {
 | 
			
		||||
                incomingCallText = _t("Incoming call from %(name)s", {name: caller});
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +78,6 @@ module.exports = React.createClass({
 | 
			
		|||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var React = require('react');
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
module.exports = React.createClass({
 | 
			
		||||
    displayName: 'VideoFeed',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,11 +16,11 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var React = require('react');
 | 
			
		||||
var ReactDOM = require('react-dom');
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
 | 
			
		||||
var sdk = require('../../../index');
 | 
			
		||||
var dis = require('../../../dispatcher');
 | 
			
		||||
import sdk from '../../../index';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
 | 
			
		||||
module.exports = React.createClass({
 | 
			
		||||
    displayName: 'VideoView',
 | 
			
		||||
| 
						 | 
				
			
			@ -53,9 +53,10 @@ module.exports = React.createClass({
 | 
			
		|||
        // this needs to be somewhere at the top of the DOM which
 | 
			
		||||
        // always exists to avoid audio interruptions.
 | 
			
		||||
        // Might as well just use DOM.
 | 
			
		||||
        var remoteAudioElement = document.getElementById("remoteAudio");
 | 
			
		||||
        const remoteAudioElement = document.getElementById("remoteAudio");
 | 
			
		||||
        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;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -70,22 +71,21 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
    onAction: function(payload) {
 | 
			
		||||
        switch (payload.action) {
 | 
			
		||||
            case 'video_fullscreen':
 | 
			
		||||
            case 'video_fullscreen': {
 | 
			
		||||
                if (!this.container) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var element = this.container;
 | 
			
		||||
                const element = this.container;
 | 
			
		||||
                if (payload.fullscreen) {
 | 
			
		||||
                    var requestMethod = (
 | 
			
		||||
                    const requestMethod = (
 | 
			
		||||
                        element.requestFullScreen ||
 | 
			
		||||
                        element.webkitRequestFullScreen ||
 | 
			
		||||
                        element.mozRequestFullScreen ||
 | 
			
		||||
                        element.msRequestFullscreen
 | 
			
		||||
                    );
 | 
			
		||||
                    requestMethod.call(element);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    var exitMethod = (
 | 
			
		||||
                } else {
 | 
			
		||||
                    const exitMethod = (
 | 
			
		||||
                        document.exitFullscreen ||
 | 
			
		||||
                        document.mozCancelFullScreen ||
 | 
			
		||||
                        document.webkitExitFullscreen ||
 | 
			
		||||
| 
						 | 
				
			
			@ -96,17 +96,18 @@ module.exports = React.createClass({
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
        var fullscreenElement = (document.fullscreenElement ||
 | 
			
		||||
        const fullscreenElement = (document.fullscreenElement ||
 | 
			
		||||
                 document.mozFullScreenElement ||
 | 
			
		||||
                 document.webkitFullscreenElement);
 | 
			
		||||
        var maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
 | 
			
		||||
        const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
 | 
			
		||||
| 
						 | 
				
			
			@ -119,5 +120,5 @@ module.exports = React.createClass({
 | 
			
		|||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,24 +14,24 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
var MatrixClientPeg = require('./MatrixClientPeg');
 | 
			
		||||
var Modal = require('./Modal');
 | 
			
		||||
var sdk = require('./index');
 | 
			
		||||
import MatrixClientPeg from './MatrixClientPeg';
 | 
			
		||||
import Modal from './Modal';
 | 
			
		||||
import sdk from './index';
 | 
			
		||||
import { _t } from './languageHandler';
 | 
			
		||||
var dis = require("./dispatcher");
 | 
			
		||||
var Rooms = require("./Rooms");
 | 
			
		||||
import dis from "./dispatcher";
 | 
			
		||||
import * as Rooms from "./Rooms";
 | 
			
		||||
 | 
			
		||||
var q = require('q');
 | 
			
		||||
import q from 'q';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 {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.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {Promise} which resolves to the room id, or null if the
 | 
			
		||||
 * action was aborted or failed.
 | 
			
		||||
 */
 | 
			
		||||
function createRoom(opts) {
 | 
			
		||||
    opts = opts || {};
 | 
			
		||||
| 
						 | 
				
			
			@ -69,16 +69,22 @@ function createRoom(opts) {
 | 
			
		|||
    createOpts.initial_state = createOpts.initial_state || [
 | 
			
		||||
        {
 | 
			
		||||
            content: {
 | 
			
		||||
                guest_access: 'can_join'
 | 
			
		||||
                guest_access: 'can_join',
 | 
			
		||||
            },
 | 
			
		||||
            type: 'm.room.guest_access',
 | 
			
		||||
            state_key: '',
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
 | 
			
		||||
 | 
			
		||||
    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() {
 | 
			
		||||
        modal.close();
 | 
			
		||||
    }).then(function(res) {
 | 
			
		||||
| 
						 | 
				
			
			@ -98,10 +104,16 @@ function createRoom(opts) {
 | 
			
		|||
                action: 'view_room',
 | 
			
		||||
                room_id: roomId,
 | 
			
		||||
                should_peek: false,
 | 
			
		||||
                // Creating a room will have joined us to the room
 | 
			
		||||
                joined: true,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return roomId;
 | 
			
		||||
    }, 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);
 | 
			
		||||
        Modal.createDialog(ErrorDialog, {
 | 
			
		||||
            title: _t("Failure to create room"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
 | 
			
		|||
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 = {
 | 
			
		||||
    looksValid: function(email) {
 | 
			
		||||
        return EMAIL_ADDRESS_REGEX.test(email);
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ limitations under the License.
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports = function(dest, src) {
 | 
			
		||||
    for (var i in src) {
 | 
			
		||||
    for (const i in src) {
 | 
			
		||||
        if (src.hasOwnProperty(i)) {
 | 
			
		||||
            dest[i] = src[i];
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -196,6 +196,7 @@
 | 
			
		|||
    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
 | 
			
		||||
    "Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room",
 | 
			
		||||
    "Changes your display nickname": "Changes your display nickname",
 | 
			
		||||
    "Changes colour scheme of current room": "Changes colour scheme of current room",
 | 
			
		||||
    "changing room on a RoomView is not supported": "changing room on a RoomView is not supported",
 | 
			
		||||
    "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
 | 
			
		||||
    "Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key",
 | 
			
		||||
| 
						 | 
				
			
			@ -421,6 +422,8 @@
 | 
			
		|||
    "Notifications": "Notifications",
 | 
			
		||||
    "(not supported by this browser)": "(not supported by this browser)",
 | 
			
		||||
    "<not supported>": "<not supported>",
 | 
			
		||||
    "AM": "AM",
 | 
			
		||||
    "PM": "PM",
 | 
			
		||||
    "NOT verified": "NOT verified",
 | 
			
		||||
    "No devices with registered encryption keys": "No devices with registered encryption keys",
 | 
			
		||||
    "No display name": "No display name",
 | 
			
		||||
| 
						 | 
				
			
			@ -512,6 +515,7 @@
 | 
			
		|||
    "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
 | 
			
		||||
    "Set": "Set",
 | 
			
		||||
    "Settings": "Settings",
 | 
			
		||||
    "Sets the room topic": "Sets the room topic",
 | 
			
		||||
    "Show Apps": "Show Apps",
 | 
			
		||||
    "Show panel": "Show panel",
 | 
			
		||||
    "Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
 | 
			
		||||
| 
						 | 
				
			
			@ -843,6 +847,7 @@
 | 
			
		|||
    "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.",
 | 
			
		||||
    "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.",
 | 
			
		||||
    "Verify device": "Verify device",
 | 
			
		||||
    "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
 | 
			
		||||
    "I verify that the keys match": "I verify that the keys match",
 | 
			
		||||
    "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.",
 | 
			
		||||
    "Unable to restore session": "Unable to restore session",
 | 
			
		||||
| 
						 | 
				
			
			@ -946,5 +951,6 @@
 | 
			
		|||
    "You are a member of these groups": "You are a member of these groups",
 | 
			
		||||
    "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.",
 | 
			
		||||
    "Join an existing group": "Join an existing group",
 | 
			
		||||
    "To join an exisitng group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.": "To join an exisitng group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>."
 | 
			
		||||
    "To join an exisitng group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.": "To join an exisitng group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.",
 | 
			
		||||
    "Autocomplete Delay (ms):": "Autocomplete Delay (ms):"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -120,6 +120,8 @@
 | 
			
		|||
    "zh-sg": "Chinese (Singapore)",
 | 
			
		||||
    "zh-tw": "Chinese (Taiwan)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "accept": "accept",
 | 
			
		||||
    "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ class LifecycleStore extends Store {
 | 
			
		|||
                    deferred_action: null,
 | 
			
		||||
                });
 | 
			
		||||
                break;
 | 
			
		||||
            case 'sync_state':
 | 
			
		||||
            case 'sync_state': {
 | 
			
		||||
                if (payload.state !== 'PREPARED') {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +61,7 @@ class LifecycleStore extends Store {
 | 
			
		|||
                });
 | 
			
		||||
                dis.dispatch(deferredAction);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case 'on_logged_out':
 | 
			
		||||
                this.reset();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (payload.joined) {
 | 
			
		||||
                newState.joining = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If an event ID wasn't specified, default to the one saved for this room
 | 
			
		||||
            // via update_scroll_state. Assume initialEventPixelOffset should be set.
 | 
			
		||||
            if (!newState.initialEventId) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												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) {
 | 
			
		||||
        var events = [];
 | 
			
		||||
    it('should handle scrollEvent strangeness', function() {
 | 
			
		||||
        const events = [];
 | 
			
		||||
 | 
			
		||||
        q().then(() => {
 | 
			
		||||
            // initialise with a few events
 | 
			
		||||
            for (var i = 0; i < 10; i++) {
 | 
			
		||||
                events.push(i+90);
 | 
			
		||||
        return q().then(() => {
 | 
			
		||||
            // initialise with a load of events
 | 
			
		||||
            for (let i = 0; i < 20; i++) {
 | 
			
		||||
                events.push(i+80);
 | 
			
		||||
            }
 | 
			
		||||
            tester.setTileKeys(events);
 | 
			
		||||
            expect(tester.fillCounts.b).toEqual(1);
 | 
			
		||||
            expect(tester.fillCounts.f).toEqual(2);
 | 
			
		||||
            expect(scrollingDiv.scrollHeight).toEqual(1550) // 10*150 + 50
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(1550 - 600);
 | 
			
		||||
            expect(scrollingDiv.scrollHeight).toEqual(3050); // 20*150 + 50
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(3050 - 600);
 | 
			
		||||
            return tester.awaitScroll();
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            expect(tester.lastScrollEvent).toBe(950);
 | 
			
		||||
            expect(tester.lastScrollEvent).toBe(3050 - 600);
 | 
			
		||||
 | 
			
		||||
            // we want to simulate back-filling as we scroll up
 | 
			
		||||
            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');
 | 
			
		||||
            tester.scrollPanel().scrollToToken("92", 0);
 | 
			
		||||
 | 
			
		||||
            // at this point, ScrollPanel will have updated scrollTop, but
 | 
			
		||||
            // the event hasn't fired. Stamp over the scrollTop.
 | 
			
		||||
            expect(tester.lastScrollEvent).toEqual(200);
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(10*150 + 200);
 | 
			
		||||
            // the event hasn't fired.
 | 
			
		||||
            expect(tester.lastScrollEvent).toEqual(3050 - 600);
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(1950);
 | 
			
		||||
 | 
			
		||||
            // now stamp over the scrollTop.
 | 
			
		||||
            console.log('faking #528');
 | 
			
		||||
            scrollingDiv.scrollTop = 500;
 | 
			
		||||
 | 
			
		||||
            return tester.awaitScroll();
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            expect(tester.lastScrollEvent).toBe(10*150 + 200);
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(10*150 + 200);
 | 
			
		||||
        }).done(done);
 | 
			
		||||
            expect(tester.lastScrollEvent).toBe(1950);
 | 
			
		||||
            expect(scrollingDiv.scrollTop).toEqual(1950);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not get stuck in #528 workaround', function(done) {
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +235,7 @@ describe('ScrollPanel', function() {
 | 
			
		|||
            tester.setTileKeys(events);
 | 
			
		||||
            expect(tester.fillCounts.b).toEqual(1);
 | 
			
		||||
            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);
 | 
			
		||||
 | 
			
		||||
            // try to scroll up, to a non-integer offset.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue