From e9132a873b870dea6f360e807ebe74b7365814d2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Nov 2015 02:13:42 +0000 Subject: [PATCH 01/59] experiment with turning the UserSettings controller into a UserSettingsStore logic class --- src/UserSettingsStore.js | 80 +++++++++++++++++++++++ src/controllers/organisms/UserSettings.js | 54 --------------- 2 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 src/UserSettingsStore.js delete mode 100644 src/controllers/organisms/UserSettings.js diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js new file mode 100644 index 0000000000..50ee03a433 --- /dev/null +++ b/src/UserSettingsStore.js @@ -0,0 +1,80 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var MatrixClientPeg = require("./MatrixClientPeg"); +var sdk = require('./index'); + +// XXX: should we be doing something here to use this as a singleton rather than +// class methods? + +module.exports = { + + // we add these wrappers to the js-sdk here in case we want to do react-specific + // dispatches or similar in future, and to give us a place to clearly separate + // business logic specific from the 'thin' react component and parent wiring component + // which actually handles the UI. + // XXX: I'm not convinced this abstraction is worth it though. + + loadProfileInfo: function() { + var cli = MatrixClientPeg.get(); + return cli.getProfileInfo(cli.credentials.userId); + }, + + saveDisplayName: function(newDisplayname) { + return MatrixClientPeg.get().setDisplayName(newDisplayname); + }, + + loadThreePids: function() { + return MatrixClientPeg.get().getThreePids(); + }, + + saveThreePids: function(threePids) { + + }, + + getEnableNotifications: function() { + var Notifier = sdk.getComponent('organisms.Notifier'); + return Notifier.isEnabled(); + }, + + setEnableNotifications: function(enable) { + var Notifier = sdk.getComponent('organisms.Notifier'); + var self = this; + if (!Notifier.supportsDesktopNotifications()) { + return; + } + Notifier.setEnabled(enable); + }, + + changePassword: function(old_password, new_password) { + var cli = MatrixClientPeg.get(); + + var authDict = { + type: 'm.login.password', + user: cli.credentials.userId, + password: old_password + }; + + this.setState({ + phase: this.Phases.Uploading, + errorString: '', + }) + + return cli.setPassword(authDict, new_password); + }, +} diff --git a/src/controllers/organisms/UserSettings.js b/src/controllers/organisms/UserSettings.js deleted file mode 100644 index d7bb8d391a..0000000000 --- a/src/controllers/organisms/UserSettings.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -var MatrixClientPeg = require("../../MatrixClientPeg"); -var q = require('q'); -var version = require('../../../package.json').version; - -module.exports = { - Phases: { - Loading: "loading", - Display: "display", - }, - - getInitialState: function() { - return { - avatarUrl: null, - threePids: [], - clientVersion: version, - phase: this.Phases.Loading, - }; - }, - - componentWillMount: function() { - var self = this; - var cli = MatrixClientPeg.get(); - - var profile_d = cli.getProfileInfo(cli.credentials.userId); - var threepid_d = cli.getThreePids(); - - q.all([profile_d, threepid_d]).then( - function(resps) { - self.setState({ - avatarUrl: resps[0].avatar_url, - threepids: resps[1].threepids, - phase: self.Phases.Display, - }); - }, - function(err) { console.err(err); } - ); - } -} From e2ae2dd199babc8a50d1d15178ae7c8582b26f09 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 00:37:56 +0000 Subject: [PATCH 02/59] merge stuff from vector --- src/components/structures/MatrixChat.js | 2 +- src/components/structures/UserSettings.js | 268 ++++++++++++++++------ src/components/views/rooms/RoomHeader.js | 5 + 3 files changed, 205 insertions(+), 70 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8ad5b44762..7148848af1 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -660,7 +660,7 @@ module.exports = React.createClass({ right_panel = break; case this.PageTypes.UserSettings: - page_element = + page_element = right_panel = break; case this.PageTypes.CreateRoom: diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 59187bb69f..aa46a7770c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -17,19 +17,24 @@ var React = require('react'); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Modal = require('../../Modal'); +var dis = require('matrix-react-sdk/lib/dispatcher') var q = require('q'); var version = require('../../../package.json').version; +var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); module.exports = React.createClass({ displayName: 'UserSettings', + Phases: { Loading: "loading", + Saving: "saving", Display: "display", }, getInitialState: function() { return { avatarUrl: null, + displayName: null, threePids: [], clientVersion: version, phase: this.Phases.Loading, @@ -38,23 +43,113 @@ module.exports = React.createClass({ componentWillMount: function() { var self = this; - var cli = MatrixClientPeg.get(); - var profile_d = cli.getProfileInfo(cli.credentials.userId); - var threepid_d = cli.getThreePids(); + var profilePromise = UserSettingsStore.loadProfileInfo(); + var threepidPromise = UserSettingsStore.loadThreePids(); - q.all([profile_d, threepid_d]).then( + q.all([profilePromise, threepidPromise]).then( function(resps) { self.setState({ avatarUrl: resps[0].avatar_url, + displayName: resps[0].displayname, threepids: resps[1].threepids, phase: self.Phases.Display, }); + + // keep a copy of the original state in order to track changes + self.setState({ + originalState: self.state + }); }, - function(err) { console.err(err); } + function(error) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't load user settings", + description: error.toString() + }); + } ); }, + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onSaveClicked: function(ev) { + var self = this; + var savePromises = []; + + // XXX: this is managed in ChangeAvatar.js, although could be moved out here in order + // to allow for the change to be staged alongside the rest of the form. + // + // if (this.state.originalState.avatarUrl !== this.state.avatarUrl) { + // savePromises.push( UserSettingsStore.saveAvatarUrl(this.state.avatarUrl) ); + // } + + if (this.state.originalState.displayName !== this.state.displayName) { + savePromises.push( UserSettingsStore.saveDisplayName(this.state.displayName) ); + } + + if (this.state.originalState.threepids.length !== this.state.threepids.length || + this.state.originalState.threepids.every(function(element, index) { + return element === this.state.threepids[index]; + })) + { + savePromises.push( UserSettingsStore.saveThreePids(this.state.threepids) ); + } + + self.setState({ + phase: self.Phases.Saving, + }); + + q.all(savePromises).then( + function(resps) { + self.setState({ + phase: self.Phases.Display, + }); + self.onClose(); + }, + function(error) { + self.setState({ + phase: self.Phases.Display, + }); + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't save user settings", + description: error.toString() + }); + } + ); + }, + + onClose: function(ev) { + // XXX: use browser history instead to find the previous room? + if (this.props.roomId) { + dis.dispatch({ + action: 'view_room', + room_id: this.props.roomId, + }); + } + else { + dis.dispatch({ + action: 'view_indexed_room', + roomIndex: 0, + }); + } + }, + + onAction: function(payload) { + if (payload.action === "notifier_enabled") { + this.setState({ + enableNotifications : UserSettingsStore.getEnableNotifications() + }); + } + }, + editAvatar: function() { var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl); var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); @@ -69,17 +164,8 @@ module.exports = React.createClass({ this.avatarDialog = Modal.createDialogWithElement(avatarDialog); }, - addEmail: function() { - - }, - - editDisplayName: function() { - this.refs.displayname.edit(); - }, - - changePassword: function() { - var ChangePassword = sdk.getComponent('settings.ChangePassword'); - Modal.createDialog(ChangePassword); + onAvatarDialogCancel: function() { + this.avatarDialog.close(); }, onLogoutClicked: function(ev) { @@ -91,72 +177,116 @@ module.exports = React.createClass({ this.logoutModal.closeDialog(); }, - onAvatarDialogCancel: function() { - this.avatarDialog.close(); + onDisplayNameChange: function(event) { + this.setState({ displayName: event.target.value }); + }, + + onEnableNotificationsChange: function(event) { + // don't bother waiting for Save to be clicked, as that'd be silly + UserSettingsStore.setEnableNotifications( this.refs.enableNotifications.value ); + this.setState({ + enableNotifications : UserSettingsStore.getEnableNotifications() + }); }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - if (this.state.phase === this.Phases.Loading) { - return - } - else if (this.state.phase === this.Phases.Display) { - var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); - var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton'); - return ( -
-
-

User Settings

-
-
-
-
- Profile Photo -
-
- Edit + var Loader = sdk.getComponent("atoms.Spinner"); + var saving; + switch (this.state.phase) { + case this.Phases.Loading: + return + case this.Phases.Saving: + saving = + case this.Phases.Display: + var RoomHeader = sdk.getComponent('molecules.RoomHeader'); + return ( +
+ + +

Profile

+ +
+
+
+
+ +
+
+ +
+
+ + {this.state.threepids.map(function(val) { + var id = "email-" + val.address; + return ( +
+
+ +
+
+ +
+
+ ); + })} + +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
-
- -
- Edit +
+
Log out
+
+ +

Notifications

+ +
+
+
+
+ +
+
+ +
+
-
- {this.state.threepids.map(function(val) { - return
{val.address}
; - })} +

Advanced

+ +
+
+ Version {this.state.clientVersion} +
-
- Add email +
+
{ saving }
+
Save and close
-
- -
-

Global Settings

-
-
-
- Change Password -
-
- Version {this.state.clientVersion} -
-
- -
-
- -
-
-
-
- ); + ); } } }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 068dff85d6..bc5c70ce08 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -73,10 +73,15 @@ module.exports = React.createClass({ var header; if (this.props.simpleHeader) { + var cancel; + if (this.props.onCancelClick) { + cancel = Close + } header =
{ this.props.simpleHeader } + { cancel }
} From 4baf9d55899754164801b9fb95168ce6a3de5066 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 00:40:01 +0000 Subject: [PATCH 03/59] Fix paths --- src/components/structures/UserSettings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index aa46a7770c..65b3db1de9 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -17,10 +17,10 @@ var React = require('react'); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Modal = require('../../Modal'); -var dis = require('matrix-react-sdk/lib/dispatcher') +var dis = require("../../dispatcher"); var q = require('q'); var version = require('../../../package.json').version; -var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); +var UserSettingsStore = require('../../UserSettingsStore'); module.exports = React.createClass({ displayName: 'UserSettings', From 08ffadc2c4921cfb4af9810494acf2ccdf2ba533 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 14:04:39 +0000 Subject: [PATCH 04/59] unbreak --- src/components/structures/UserSettings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 65b3db1de9..9a62149023 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -62,7 +62,7 @@ module.exports = React.createClass({ }); }, function(error) { - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", description: error.toString() @@ -117,7 +117,7 @@ module.exports = React.createClass({ self.setState({ phase: self.Phases.Display, }); - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't save user settings", description: error.toString() @@ -190,7 +190,7 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("atoms.Spinner"); + var Loader = sdk.getComponent("elements.Spinner"); var saving; switch (this.state.phase) { case this.Phases.Loading: @@ -198,7 +198,7 @@ module.exports = React.createClass({ case this.Phases.Saving: saving = case this.Phases.Display: - var RoomHeader = sdk.getComponent('molecules.RoomHeader'); + var RoomHeader = sdk.getComponent('rooms.RoomHeader'); return (
From c6d02b2c261361b7c9db5eea9c11c2c43b5000ea Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 21 Dec 2015 10:38:37 +0000 Subject: [PATCH 05/59] Move tab-complete logic out from MessageComposer Moved to a `TabComplete` class. Make it more generic (list of strings rather than RoomMembers) and sort the member list by last_active_ago. Everything still seems to work. --- src/TabComplete.js | 151 ++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 151 +++++------------- 2 files changed, 191 insertions(+), 111 deletions(-) create mode 100644 src/TabComplete.js diff --git a/src/TabComplete.js b/src/TabComplete.js new file mode 100644 index 0000000000..5a6bfa8343 --- /dev/null +++ b/src/TabComplete.js @@ -0,0 +1,151 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const DELAY_TIME_MS = 500; +const KEY_TAB = 9; +const KEY_SHIFT = 16; + +class TabComplete { + + constructor(opts) { + opts.startingWordSuffix = opts.startingWordSuffix || ""; + opts.wordSuffix = opts.wordSuffix || ""; + this.opts = opts; + + this.tabStruct = { + completing: false, + original: null, + index: 0 + }; + this.list = []; + this.textArea = opts.textArea; + } + + /** + * @param {String[]} completeList + */ + setCompletionList(completeList) { + this.list = completeList; + } + + setTextArea(textArea) { + this.textArea = textArea; + } + + next() { + this.tabStruct.index++; + this.setCompletionOption(); + } + + prev() { + this.tabStruct.index --; + if (this.tabStruct.index < 0) { + // wrap to the last search match, and fix up to a real index + // value after we've matched. + this.tabStruct.index = Number.MAX_VALUE; + } + this.setCompletionOption(); + } + + setCompletionOption() { + var searchIndex = 0; + var targetIndex = this.tabStruct.index; + var text = this.tabStruct.original; + + var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text); + // console.log("Searched in '%s' - got %s", text, search); + if (targetIndex === 0) { // 0 is always the original text + this.textArea.value = text; + } + else if (search && search[1]) { + // console.log("search found: " + search+" from "+text); + var expansion; + + // FIXME: could do better than linear search here + for (var i=0; i < this.list.length; i++) { + if (searchIndex < targetIndex) { + if (this.list[i].toLowerCase().indexOf(search[1].toLowerCase()) === 0) { + expansion = this.list[i]; + searchIndex++; + } + } + } + + if (searchIndex === targetIndex || targetIndex === Number.MAX_VALUE) { + if (search[0].length === text.length) { + expansion += this.opts.startingWordSuffix; + } + else { + expansion += this.opts.wordSuffix; + } + this.textArea.value = text.replace( + /@?([a-zA-Z0-9_\-:\.]+)$/, expansion + ); + // cancel blink + this.textArea.style["background-color"] = ""; + if (targetIndex === Number.MAX_VALUE) { + // wrap the index around to the last index found + this.tabStruct.index = searchIndex; + targetIndex = searchIndex; + } + } + else { + // console.log("wrapped!"); + this.textArea.style["background-color"] = "#faa"; + setTimeout(() => { // yay for lexical 'this'! + this.textArea.style["background-color"] = ""; + }, 150); + this.textArea.value = text; + this.tabStruct.index = 0; + } + } + else { + this.tabStruct.index = 0; + } + } + + onKeyDown(ev) { + if (ev.keyCode !== KEY_TAB) { + if (ev.keyCode !== KEY_SHIFT && this.tabStruct.completing) { + // they're resuming typing; reset tab complete state vars. + this.tabStruct.completing = false; + this.tabStruct.index = 0; + } + return false; + } + // init struct if necessary + if (!this.tabStruct.completing) { + this.tabStruct.completing = true; + this.tabStruct.index = 0; + // cache starting text + this.tabStruct.original = this.textArea.value; + } + + if (ev.shiftKey) { + this.prev(); + } + else { + this.next(); + } + // prevent the default TAB operation (typically focus shifting) + ev.preventDefault(); + return true; + } + +}; + + +module.exports = TabComplete; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 7c228b5c9d..3978ef2154 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -31,6 +31,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg"); var SlashCommands = require("../../../SlashCommands"); var Modal = require("../../../Modal"); var CallHandler = require('../../../CallHandler'); +var TabComplete = require("../../../TabComplete"); var sdk = require('../../../index'); var dis = require("../../../dispatcher"); @@ -67,11 +68,6 @@ module.exports = React.createClass({ componentWillMount: function() { this.oldScrollHeight = 0; this.markdownEnabled = MARKDOWN_ENABLED; - this.tabStruct = { - completing: false, - original: null, - index: 0 - }; var self = this; this.sentHistory = { // The list of typed messages. Index 0 is more recent @@ -172,6 +168,14 @@ module.exports = React.createClass({ this.props.room.roomId ); this.resizeInput(); + + // xchat-style tab complete, add a colon if tab + // completing at the start of the text + this._tabComplete = new TabComplete({ + textArea: this.refs.textarea, + startingWordSuffix: ": ", + wordSuffix: " " + }); }, componentWillUnmount: function() { @@ -198,11 +202,38 @@ module.exports = React.createClass({ this.onEnter(ev); } else if (ev.keyCode === KeyCode.TAB) { - var members = []; + var memberList = []; if (this.props.room) { - members = this.props.room.getJoinedMembers(); + // TODO: We should cache this list and only update it when the + // member list changes + memberList = this.props.room.getJoinedMembers().sort(function(a, b) { + var userA = a.user; + var userB = b.user; + if (userA && !userB) { + return -1; // a comes first + } + else if (!userA && userB) { + return 1; // b comes first + } + else if (!userA && !userB) { + return 0; // don't care + } + else { // both User objects exist + if (userA.lastActiveAgo < userB.lastActiveAgo) { + return -1; // a comes first + } + else if (userA.lastActiveAgo > userB.lastActiveAgo) { + return 1; // b comes first + } + else { + return 0; // same last active ago + } + } + }).map(function(m) { + return m.name || m.userId; + }); } - this.onTab(ev, members); + this._tabComplete.setCompletionList(memberList); } else if (ev.keyCode === KeyCode.UP) { var input = this.refs.textarea.value; @@ -222,11 +253,7 @@ module.exports = React.createClass({ this.resizeInput(); } } - else if (ev.keyCode !== KeyCode.SHIFT && this.tabStruct.completing) { - // they're resuming typing; reset tab complete state vars. - this.tabStruct.completing = false; - this.tabStruct.index = 0; - } + this._tabComplete.onKeyDown(ev); var self = this; setTimeout(function() { @@ -346,104 +373,6 @@ module.exports = React.createClass({ ev.preventDefault(); }, - onTab: function(ev, sortedMembers) { - var textArea = this.refs.textarea; - if (!this.tabStruct.completing) { - this.tabStruct.completing = true; - this.tabStruct.index = 0; - // cache starting text - this.tabStruct.original = textArea.value; - } - - // loop in the right direction - if (ev.shiftKey) { - this.tabStruct.index --; - if (this.tabStruct.index < 0) { - // wrap to the last search match, and fix up to a real index - // value after we've matched. - this.tabStruct.index = Number.MAX_VALUE; - } - } - else { - this.tabStruct.index++; - } - - var searchIndex = 0; - var targetIndex = this.tabStruct.index; - var text = this.tabStruct.original; - - var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text); - // console.log("Searched in '%s' - got %s", text, search); - if (targetIndex === 0) { // 0 is always the original text - textArea.value = text; - } - else if (search && search[1]) { - // console.log("search found: " + search+" from "+text); - var expansion; - - // FIXME: could do better than linear search here - for (var i=0; i= targetIndex) { - break; - } - var userId = sortedMembers[i].userId; - // === 1 because mxids are @username - if (userId.toLowerCase().indexOf(search[1].toLowerCase()) === 1) { - expansion = userId; - searchIndex++; - } - } - } - - if (searchIndex === targetIndex || - targetIndex === Number.MAX_VALUE) { - // xchat-style tab complete, add a colon if tab - // completing at the start of the text - if (search[0].length === text.length) { - expansion += ": "; - } - else { - expansion += " "; - } - textArea.value = text.replace( - /@?([a-zA-Z0-9_\-:\.]+)$/, expansion - ); - // cancel blink - textArea.style["background-color"] = ""; - if (targetIndex === Number.MAX_VALUE) { - // wrap the index around to the last index found - this.tabStruct.index = searchIndex; - targetIndex = searchIndex; - } - } - else { - // console.log("wrapped!"); - textArea.style["background-color"] = "#faa"; - setTimeout(function() { - textArea.style["background-color"] = ""; - }, 150); - textArea.value = text; - this.tabStruct.index = 0; - } - } - else { - this.tabStruct.index = 0; - } - // prevent the default TAB operation (typically focus shifting) - ev.preventDefault(); - }, - onTypingActivity: function() { this.isTyping = true; if (!this.userTypingTimer) { From 26dc3cc553e49ee889e5579c32ca022cc7126961 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 21 Dec 2015 10:59:10 +0000 Subject: [PATCH 06/59] Push up instantiation of TabComplete to RoomView RoomView is the parent component which creates MessageComposer AND the status bar. By making RoomView instantiate TabComplete we can scope instances correctly rather than relying on singleton behaviour through dispatches. This also makes communication between status bar and the MessageComposer infinitely easier since they are now sharing the same TabComplete object. --- src/TabComplete.js | 16 ++++++++++++ src/components/structures/RoomView.js | 11 +++++++- src/components/views/rooms/MessageComposer.js | 25 +++++++++++-------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 5a6bfa8343..c49ef19e7f 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -45,6 +45,10 @@ class TabComplete { this.textArea = textArea; } + isTabCompleting() { + return this.tabStruct.completing; + } + next() { this.tabStruct.index++; this.setCompletionOption(); @@ -117,15 +121,27 @@ class TabComplete { } } + /** + * @param {DOMEvent} e + * @return {Boolean} True if the tab complete state changed as a result of + * this event. + */ onKeyDown(ev) { if (ev.keyCode !== KEY_TAB) { if (ev.keyCode !== KEY_SHIFT && this.tabStruct.completing) { // they're resuming typing; reset tab complete state vars. this.tabStruct.completing = false; this.tabStruct.index = 0; + return true; } return false; } + + if (!this.textArea) { + console.error("onKeyDown called before a