From 6ff41c40b6b7a1329d571c7b62c2b6df08f6cc5d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 24 Mar 2016 11:25:41 +0000 Subject: [PATCH] Split a textinput component out of MessageComposer Split the text entry section out of MessageComposer: it has a lot of stuff which won't be needed if we disable input --- src/component-index.js | 1 + src/components/views/rooms/MessageComposer.js | 427 +--------------- .../views/rooms/MessageComposerInput.js | 456 ++++++++++++++++++ 3 files changed, 470 insertions(+), 414 deletions(-) create mode 100644 src/components/views/rooms/MessageComposerInput.js diff --git a/src/component-index.js b/src/component-index.js index 67c09e61a6..e15f0e917f 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -81,6 +81,7 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); +module.exports.components['views.rooms.MessageComposerInput'] = require('./components/views/rooms/MessageComposerInput'); module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index c3e02975e0..bd84fd85be 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -13,431 +13,32 @@ 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 React = require('react'); -var marked = require("marked"); -marked.setOptions({ - renderer: new marked.Renderer(), - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false -}); - -var MatrixClientPeg = require("../../../MatrixClientPeg"); -var SlashCommands = require("../../../SlashCommands"); -var Modal = require("../../../Modal"); var CallHandler = require('../../../CallHandler'); -var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; +var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +var dis = require('../../../dispatcher'); -var dis = require("../../../dispatcher"); -var KeyCode = { - ENTER: 13, - BACKSPACE: 8, - DELETE: 46, - TAB: 9, - SHIFT: 16, - UP: 38, - DOWN: 40 -}; - -var TYPING_USER_TIMEOUT = 10000; -var TYPING_SERVER_TIMEOUT = 30000; -var MARKDOWN_ENABLED = true; - -function mdownToHtml(mdown) { - var html = marked(mdown) || ""; - html = html.trim(); - // strip start and end

tags else you get 'orrible spacing - if (html.indexOf("

") === 0) { - html = html.substring("

".length); - } - if (html.lastIndexOf("

") === (html.length - "

".length)) { - html = html.substring(0, html.length - "

".length); - } - return html; -} module.exports = React.createClass({ displayName: 'MessageComposer', - statics: { - // the height we limit the composer to - MAX_HEIGHT: 100, - }, - propTypes: { tabComplete: React.PropTypes.any, // a callback which is called when the height of the composer is // changed due to a change in content. onResize: React.PropTypes.func, - }, - componentWillMount: function() { - this.oldScrollHeight = 0; - this.markdownEnabled = MARKDOWN_ENABLED; - var self = this; - this.sentHistory = { - // The list of typed messages. Index 0 is more recent - data: [], - // The position in data currently displayed - position: -1, - // The room the history is for. - roomId: null, - // The original text before they hit UP - originalText: null, - // The textarea element to set text to. - element: null, + // js-sdk Room object + room: React.PropTypes.object.isRequired, - init: function(element, roomId) { - this.roomId = roomId; - this.element = element; - this.position = -1; - var storedData = window.sessionStorage.getItem( - "history_" + roomId - ); - if (storedData) { - this.data = JSON.parse(storedData); - } - if (this.roomId) { - this.setLastTextEntry(); - } - }, + // string representing the current voip call state + callState: React.PropTypes.string, - push: function(text) { - // store a message in the sent history - this.data.unshift(text); - window.sessionStorage.setItem( - "history_" + this.roomId, - JSON.stringify(this.data) - ); - // reset history position - this.position = -1; - this.originalText = null; - }, - - // move in the history. Returns true if we managed to move. - next: function(offset) { - if (this.position === -1) { - // user is going into the history, save the current line. - this.originalText = this.element.value; - } - else { - // user may have modified this line in the history; remember it. - this.data[this.position] = this.element.value; - } - - if (offset > 0 && this.position === (this.data.length - 1)) { - // we've run out of history - return false; - } - - // retrieve the next item (bounded). - var newPosition = this.position + offset; - newPosition = Math.max(-1, newPosition); - newPosition = Math.min(newPosition, this.data.length - 1); - this.position = newPosition; - - if (this.position !== -1) { - // show the message - this.element.value = this.data[this.position]; - } - else if (this.originalText !== undefined) { - // restore the original text the user was typing. - this.element.value = this.originalText; - } - - self.resizeInput(); - return true; - }, - - saveLastTextEntry: function() { - // save the currently entered text in order to restore it later. - // NB: This isn't 'originalText' because we want to restore - // sent history items too! - var text = this.element.value; - window.sessionStorage.setItem("input_" + this.roomId, text); - }, - - setLastTextEntry: function() { - var text = window.sessionStorage.getItem("input_" + this.roomId); - if (text) { - this.element.value = text; - self.resizeInput(); - } - } - }; - }, - - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - this.sentHistory.init( - this.refs.textarea, - this.props.room.roomId - ); - this.resizeInput(); - if (this.props.tabComplete) { - this.props.tabComplete.setTextArea(this.refs.textarea); - } - }, - - componentWillUnmount: function() { - dis.unregister(this.dispatcherRef); - this.sentHistory.saveLastTextEntry(); - }, - - onAction: function(payload) { - var textarea = this.refs.textarea; - switch (payload.action) { - case 'focus_composer': - textarea.focus(); - break; - case 'insert_displayname': - if (textarea.value.length) { - var left = textarea.value.substring(0, textarea.selectionStart); - var right = textarea.value.substring(textarea.selectionEnd); - if (right.length) { - left += payload.displayname; - } - else { - left = left.replace(/( ?)$/, " " + payload.displayname); - } - textarea.value = left + right; - textarea.focus(); - textarea.setSelectionRange(left.length, left.length); - } - else { - textarea.value = payload.displayname + ": "; - textarea.focus(); - } - break; - } - }, - - onKeyDown: function (ev) { - if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) { - var input = this.refs.textarea.value; - if (input.length === 0) { - ev.preventDefault(); - return; - } - this.sentHistory.push(input); - this.onEnter(ev); - } - else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) { - var oldSelectionStart = this.refs.textarea.selectionStart; - // Remember the keyCode because React will recycle the synthetic event - var keyCode = ev.keyCode; - // set a callback so we can see if the cursor position changes as - // a result of this event. If it doesn't, we cycle history. - setTimeout(() => { - if (this.refs.textarea.selectionStart == oldSelectionStart) { - this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1); - this.resizeInput(); - } - }, 0); - } - - if (this.props.tabComplete) { - this.props.tabComplete.onKeyDown(ev); - } - - var self = this; - setTimeout(function() { - if (self.refs.textarea && self.refs.textarea.value != '') { - self.onTypingActivity(); - } else { - self.onFinishedTyping(); - } - }, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :( - }, - - resizeInput: function() { - // scrollHeight is at least equal to clientHeight, so we have to - // temporarily crimp clientHeight to 0 to get an accurate scrollHeight value - this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS - var newHeight = Math.min(this.refs.textarea.scrollHeight, - this.constructor.MAX_HEIGHT); - this.refs.textarea.style.height = Math.ceil(newHeight) + "px"; - this.oldScrollHeight = this.refs.textarea.scrollHeight; - - if (this.props.onResize) { - // kick gemini-scrollbar to re-layout - this.props.onResize(); - } - }, - - onKeyUp: function(ev) { - if (this.refs.textarea.scrollHeight !== this.oldScrollHeight || - ev.keyCode === KeyCode.DELETE || - ev.keyCode === KeyCode.BACKSPACE) - { - this.resizeInput(); - } - }, - - onEnter: function(ev) { - var contentText = this.refs.textarea.value; - - // bodge for now to set markdown state on/off. We probably want a separate - // area for "local" commands which don't hit out to the server. - if (contentText.indexOf("/markdown") === 0) { - ev.preventDefault(); - this.refs.textarea.value = ''; - if (contentText.indexOf("/markdown on") === 0) { - this.markdownEnabled = true; - } - else if (contentText.indexOf("/markdown off") === 0) { - this.markdownEnabled = false; - } - else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Unknown command", - description: "Usage: /markdown on|off" - }); - } - return; - } - - var cmd = SlashCommands.processInput(this.props.room.roomId, contentText); - if (cmd) { - ev.preventDefault(); - if (!cmd.error) { - this.refs.textarea.value = ''; - } - if (cmd.promise) { - cmd.promise.done(function() { - console.log("Command success."); - }, function(err) { - console.error("Command failure: %s", err); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Server error", - description: err.message - }); - }); - } - else if (cmd.error) { - console.error(cmd.error); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Command error", - description: cmd.error - }); - } - return; - } - - var isEmote = /^\/me( |$)/i.test(contentText); - var sendMessagePromise; - - if (isEmote) { - contentText = contentText.substring(4); - } - else if (contentText[0] === '/') { - contentText = contentText.substring(1); - } - - var htmlText; - if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) { - sendMessagePromise = isEmote ? - MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) : - MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); - } - else { - sendMessagePromise = isEmote ? - MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : - MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); - } - - sendMessagePromise.done(function() { - dis.dispatch({ - action: 'message_sent' - }); - }, function() { - dis.dispatch({ - action: 'message_send_failed' - }); - }); - this.refs.textarea.value = ''; - this.resizeInput(); - ev.preventDefault(); - }, - - onTypingActivity: function() { - this.isTyping = true; - if (!this.userTypingTimer) { - this.sendTyping(true); - } - this.startUserTypingTimer(); - this.startServerTypingTimer(); - }, - - onFinishedTyping: function() { - this.isTyping = false; - this.sendTyping(false); - this.stopUserTypingTimer(); - this.stopServerTypingTimer(); - }, - - startUserTypingTimer: function() { - this.stopUserTypingTimer(); - var self = this; - this.userTypingTimer = setTimeout(function() { - self.isTyping = false; - self.sendTyping(self.isTyping); - self.userTypingTimer = null; - }, TYPING_USER_TIMEOUT); - }, - - stopUserTypingTimer: function() { - if (this.userTypingTimer) { - clearTimeout(this.userTypingTimer); - this.userTypingTimer = null; - } - }, - - startServerTypingTimer: function() { - if (!this.serverTypingTimer) { - var self = this; - this.serverTypingTimer = setTimeout(function() { - if (self.isTyping) { - self.sendTyping(self.isTyping); - self.startServerTypingTimer(); - } - }, TYPING_SERVER_TIMEOUT / 2); - } - }, - - stopServerTypingTimer: function() { - if (this.serverTypingTimer) { - clearTimeout(this.servrTypingTimer); - this.serverTypingTimer = null; - } - }, - - sendTyping: function(isTyping) { - MatrixClientPeg.get().sendTyping( - this.props.room.roomId, - this.isTyping, TYPING_SERVER_TIMEOUT - ).done(); - }, - - refreshTyping: function() { - if (this.typingTimeout) { - clearTimeout(this.typingTimeout); - this.typingTimeout = null; - } - }, - - onInputClick: function(ev) { - this.refs.textarea.focus(); + // callback when a file to upload is chosen + uploadFile: React.PropTypes.func.isRequired, }, onUploadClick: function(ev) { @@ -446,7 +47,7 @@ module.exports = React.createClass({ onUploadFileSelected: function(ev) { var files = ev.target.files; - // MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file + // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file if (files && files.length > 0) { this.props.uploadFile(files[0]); } @@ -488,10 +89,9 @@ module.exports = React.createClass({ var uploadInputStyle = {display: 'none'}; var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var TintableSvg = sdk.getComponent("elements.TintableSvg"); + var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); var callButton, videoCallButton, hangupButton; - var call = CallHandler.getCallForRoom(this.props.room.roomId); - //var call = CallHandler.getAnyActiveCall(); if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
@@ -516,9 +116,8 @@ module.exports = React.createClass({
-
-