Merge remote-tracking branch 'origin/develop' into dbkr/groups_better_groupview
						commit
						c21f90338d
					
				|  | @ -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'); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -76,10 +76,13 @@ class ScalarAuthClient { | |||
|         return defer.promise; | ||||
|     } | ||||
| 
 | ||||
|     getScalarInterfaceUrlForRoom(roomId) { | ||||
|     getScalarInterfaceUrlForRoom(roomId, screen) { | ||||
|         var url = SdkConfig.get().integrations_ui_url; | ||||
|         url += "?scalar_token=" + encodeURIComponent(this.scalarToken); | ||||
|         url += "&room_id=" + encodeURIComponent(roomId); | ||||
|         if (screen) { | ||||
|             url += '&screen=' + encodeURIComponent(screen); | ||||
|         } | ||||
|         return url; | ||||
|     } | ||||
| 
 | ||||
|  | @ -89,4 +92,3 @@ class ScalarAuthClient { | |||
| } | ||||
| 
 | ||||
| module.exports = ScalarAuthClient; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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>; | ||||
|     } | ||||
|  |  | |||
|  | @ -103,8 +103,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" || | ||||
|  |  | |||
|  | @ -176,7 +176,7 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); | ||||
|         const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? | ||||
|                 this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : | ||||
|                 this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') : | ||||
|                 null; | ||||
|         Modal.createDialog(IntegrationsManager, { | ||||
|             src: src, | ||||
|  | @ -187,7 +187,7 @@ module.exports = React.createClass({ | |||
|         const apps = this.state.apps.map( | ||||
|             (app, index, arr) => { | ||||
|                 return <AppTile | ||||
|                     key={app.name} | ||||
|                     key={app.id} | ||||
|                     id={app.id} | ||||
|                     url={app.url} | ||||
|                     name={app.name} | ||||
|  |  | |||
|  | @ -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", | ||||
|  | @ -947,5 +952,6 @@ | |||
|     "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>.", | ||||
|   "Featured Rooms:": "Featured Rooms:" | ||||
|     "Featured Rooms:": "Featured Rooms:", | ||||
|     "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
	
	 David Baker
						David Baker