diff --git a/README.md b/README.md index 166eec49..73a6775a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # Single-page chat app to show Tinode functionality -Example using Tinode to build a single-page chat application. The app uses bootstrap, jquery (bootstrap dependency to make it dynamic), react.js. Tinode js library has no external dependencies. +Example using Tinode to build a single-page chat application. The app uses bootstrap, +[React](https://facebook.github.io/react/), Google's [material design fonts](https://www.google.com/fonts/) +and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). Tinode javascript library has no +external dependencies. -This is work in progress. Bugs are expected. +This is work in progress. Bugs are expected. The work was tested in the latest Chrome only. Try it at http://api.tinode.co/x/example-react-js/ License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) Avatar photos from [https://www.pexels.com/] under [CC0 license](https://www.pexels.com/photo-license/). + +Background pattern from [http://subtlepatterns.com/], commercial and non-commercial use allowed with attribution. diff --git a/css/base.css b/css/base.css index c737f9d3..4062992f 100644 --- a/css/base.css +++ b/css/base.css @@ -1,124 +1,197 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + html, body { - margin:0; - padding:0; height:100%; width:100%; - overflow-x: hidden; + font-family: 'Roboto', Arial, sans-serif; + overflow: hidden; + background-color: silver; } -.fullScreen { - height:100%; - width:100%; -} - -/* Top-level layout style: sidepanel + main view */ #mountPoint { - padding-left: 0; - transition: all 0.5s ease; + height: 100%; + padding-top: 1em; + padding-bottom: 4em; +} + +/* Top-level application layout style: sidepanel + main view */ +#app-container { + position: relative; + display: flex; + height: 100%; + width: 80em; + overflow: hidden; + margin-left: auto; + margin-right: auto; + box-shadow: 1px 2px 0.3em gray; + background-color: white; +} + +#sidepanel { + display: flex; + flex-direction: column; + width: 30em; height: 100%; } -#mountPoint.toggled { - padding-left: 30em; -} - -#sidepanel-wrapper { - position: fixed; - left: 30em; - width: 0; - height: 100vh; - margin-left: -30em; - transition: all 0.5s ease; -} - -#mountPoint.toggled #sidepanel-wrapper { - width: 30em; -} - -#page-content-wrapper { +#topic-view { + display: flex; + flex-direction: column; width: 100%; height: 100%; - position: absolute; -} - -#mountPoint.toggled #page-content-wrapper { - position: absolute; - margin-right: -30em; -} - -.sidepanel-nav { - position: absolute; - top: 0; - width: 30em; - margin: 0; - padding: 0; - height: 100%; -} - -@media(min-width:768px) { - #mountPoint { - padding-left: 30em; - } - - #mountPoint.toggled { - padding-left: 0; - } - - #sidepanel-wrapper { - width: 30em; - } - - #mountPoint.toggled #sidepanel-wrapper { - width: 0; - } - - #page-content-wrapper { - position: relative; - height: 100%; - } - - #mountPoint.toggled #page-content-wrapper { - position: relative; - margin-right: 0; - } } /* END of top-level layout */ -/* Making the alert box less tall */ -.alert-block { - padding-top: 0.25em; - padding-bottom: 0.25em; - margin-bottom: 3px; - height: 2.3em; +/* Sidepanel styles */ +#side-caption-panel { + display: flex; + align-items: center; + x-justify-content: space-between; + flex: none; + padding: 0.5em; + height: 4.5em; + + background-color: #039be5; /* light-blue 600 */ + color: #b3e5fc; /* light-blue 100 */ + + box-shadow: 0 1px 1px -1px gray; + z-index: 1; /* otherwise the shadow becomes invisible */ } -.alert-error { - background-color: #FCC; - border-color: #C99; +#side-caption-panel a { + color: #b3e5fc; /* light-blue 100 */ + padding-left: 0.5em; + vertical-align: -1.5em; /* icons are bigger than the declared font size */ +} + +#side-caption-panel :hover { + color: white; +} + +#sidepanel-title { + margin-right: auto; + margin-left: 0.5em; + font-size: 2rem; +} + +#self-avatar { + width: 3em; + height: auto; + flex: none; +} + +/* Area which displays an error message */ +#alert-box { + padding: 0.5em; + background-color: transparent; +} +#alert-box.error-alert { + background-color: #ffcdd2; + border-color: #d32f2f; color: #600; } -.avatar { - width: 32pt; +/* List of contacts */ +#contacts-panel { + position: relative; + overflow-y: scroll; +} + +#contact-box { + list-style: none; +} + +#contact-box li { + display: flex; + padding: 0.5em; + min-height: 3.5em; + overflow: hidden; +} +#contact-box li:hover { + background-color: #fffde7; /* yellow 50 highlight */ +} + +/* Avatar and online-offline indicator */ +#contact-box .avatar-box { + margin-right: 1em; + position: relative; + width: 3em; + height: 3em; + overflow: hidden; + padding: 0; +} +#contact-box .avatar { + width: 3em; height: auto; } -.navbar { - height: 64px; - margin-bottom: 0.2em; +/* Online-offline indicator positioned inside avatar-box */ +#contact-box .online, .offline { + font-size: 1.5em; + position: absolute; + right: 0; + bottom: -0.5em; +} +#contact-box .online { + color: #66bb6a; +} +#contact-box .offline { + color: #999; } -.navbar .avatar { - margin-left: 4pt; - margin-right: 4pt; - margin-top: 8pt; +#add-topic { + position: absolute; + right: 1em; + bottom: 1em; +} +/* END of avatar box */ + +/* END of sidepanel styles */ + +/* Topic/messages view */ +#topic-caption-panel { + display: flex; + align-items: baseline; + justify-content: space-between; + flex: none; + padding: 0.5em; + height: 4.5em; + background-color: #b0bec5; + box-shadow: 0 1px 1px -1px gray; + z-index: 1; /* otherwise the shadow becomes invisible */ } -.label-as-badge { - border-radius: 1em; - margin-left: 0.5em; +#messages-panel { + padding: 0.75em; + overflow-y: scroll; + background-color: #eceff1; + background-image: url("../img/ignasi_pattern_s.png"); } +/* Footer-panel with the input for the new message */ +#send-message-panel { + flex: none; + margin-top: auto; + padding: 0.5em; +} + +/* A static panel shown when no topic is selected */ +#dummy-view { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #e0f7fa; +} + +/* END of topic/mesages view */ + + .contactBox { overflow: hidden; } @@ -130,7 +203,6 @@ html, body { } .contactList { - z-index: 1; padding: 9.5em 0em 0.5em 0em; margin-top: -9em; height: 100vh; @@ -149,18 +221,6 @@ html, body { margin-top: 0; } -.online, .offline { - font-size: 14pt; -} - -.online { - color: #8B7; -} - -.offline { - color: #999; -} - .fullHeight { height: 100%; overflow: hidden; @@ -176,135 +236,11 @@ html, body { margin: 0; } -#chatTitle { - z-index: 3; - position: absolute; - left: 0; - right: 0; - top: 0; - height: 64px; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15); -} - -#messagesPanel { - overflow-y: scroll; - padding-top: 0; - padding-bottom: 0; - background-image: url("../img/ignasi_pattern_s.png"); -} - -#messagesView { - padding-top: 70px; -} - -#sendMessagePanel { - position: absolute; - bottom: 0; - box-shadow: 0px -1px 2px rgba(0, 0, 0, 0.15); -} - -#dummyView { - display:flex; - align-items: center; - justify-content: center; - background-color: #E9E9FF; -} - -/* -.chatBox { - list-style: none; - padding: 1.5em 0 1.5em; - position: relative; -} - -.chatBox > li { - margin-bottom: 1em; - position: relative; -} - -.chatBox > li:before, -.chatBox > li:after { - content: " "; - display: table; -} - -.chatBox > li:after { - clear: both; -} - -.chatBox > li > .chatBox-panel { - max-width: 90%; - float: right; - background-color: #DFD; - border: 1px solid #CDC; - border-radius: 4px; - padding: 1em; - position: relative; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15); -} - -.chatBox > li > .chatBox-panel:before { - position: absolute; - top: 9px; - right: -12px; - display: inline-block; - border-top: 12px solid transparent; - border-left: 12px solid #CCC; - border-right: 0px solid #CCC; - border-bottom: 12px solid transparent; - content: " "; -} - -.chatBox > li > .chatBox-panel:after { - position: absolute; - top: 10px; - right: -11px; - display: inline-block; - border-top: 11px solid transparent; - border-left: 11px solid #DFD; - border-right: 0 solid #DDF; - border-bottom: 11px solid transparent; - content: " "; -} - -.chatBox > li.chatBox-inverted > .chatBox-panel { - background-color: #DDF; - border: 1px solid #CCD; - float: left; -} - -.chatBox > li.chatBox-inverted > .chatBox-panel:before { - border-left-width: 0; - border-right-width: 12px; - left: -12px; - right: auto; -} - -.chatBox > li.chatBox-inverted > .chatBox-panel:after { - border-left-width: 0; - border-right-width: 11px; - left: -11px; - right: auto; -} - -.chatBox .timestamp { - padding-left: 3em; -} - -.chatBox-body > p, -.chatBox-body > ul { - margin-bottom: 0; -} - -.chatBox-body > p + p { - margin-top: 0.5em; -} -*/ - -/* CSS talk bubble */ +/* Talk bubbles */ .chat-box { list-style: none; - padding: 0 0 5em 0; + padding: 0 0 3.5em 0; + margin: 0 1em 0 1em; } .chat-box li { @@ -321,56 +257,40 @@ html, body { .chat-box .bubble { position: relative; max-width: 32em; + min-width: 16em; height: auto; - padding: 1.5em 0 1.5em; - background-color: #ef9a9a; - border: 1px solid #e53935; - + padding: 0.75em 0.5em 0.75em 1.25em; margin: 0; - padding: 0.5em; text-align: left; -} -.chat-box li.single { - margin-top: 0.5em; - margin-bottom: 0.5em; -} - -.chat-box li.single .bubble { border-radius: 0.5em; -webkit-border-radius: 0.5em; -moz-border-radius: 0.5em; + + /* tested in chrome 47 on mac - performance sucks + -webkit-filter: drop-shadow(0px 1px 1px -1px rgba(0,0,0,0.3)); + -filter: drop-shadow(0px 1px 1px -1px rgba(0,0,0,0.3)); + */ +} + +.chat-box li.single { + margin-top: 0.75em; + margin-bottom: 0.75em; } .chat-box li.first { - margin-top: 0.5em; - margin-bottom: 0.05em; -} - -.chat-box li.first .bubble { - border-radius: 0.5em 0.5em 0 0; - -webkit-border-radius: 0.5em 0.5em 0 0; - -moz-border-radius: 0.5em 0.5em 0 0; + margin-top: 0.75em; + margin-bottom: 0.15em; } .chat-box li.middle { - margin-top: 0.05em; - margin-bottom: 0.05em; -} - -.chat-box li.middle .bubble { - border-radius: 0; + margin-top: 0.15em; + margin-bottom: 0.15em; } .chat-box li.last { - margin-top: 0.05em; - margin-bottom: 0.5em; -} - -.chat-box li.last .bubble { - border-radius: 0 0 0.5em 0.5em; - -webkit-border-radius: 0 0 0.5em 0.5em; - -moz-border-radius: 0 0 0.5em 0.5em; + margin-top: 0.15em; + margin-bottom: 0.75em; } /* Left triangle placed bottom left flush. */ @@ -380,23 +300,14 @@ html, body { width: 0; height: 0; right: auto; - left: -11px; - top: auto; - bottom: -1px; /* border-width */ - border: 10px solid; - border-color: transparent transparent #e53935 transparent; -} -.chat-box li.left .tip::after{ - content: ''; - position: absolute; - width: 0; - height: 0; - right: auto; - left: -9px; + left: -10px; top: auto; bottom: 0; - border: 9px solid; - border-color: transparent transparent #ef9a9a transparent; + border: 10px solid; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: inherit; + border-left-color: transparent; } /* Right triangle placed bottom right flush. */ @@ -406,23 +317,14 @@ html, body { width: 0; height: 0; left: auto; - right: -11px; - top: auto; - bottom: -1px; /* border-width */ - border: 10px solid; - border-color: transparent transparent #e53935 transparent; -} -.chat-box li.right .tip::after { - content: ''; - position: absolute; - width: 0; - height: 0; - left: auto; - right: -9px; + right: -10px; top: auto; bottom: 0; - border: 9px solid; - border-color: transparent transparent #ef9a9a transparent; + border: 10px solid; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: inherit; + border-left-color: transparent; } .chat-box .avatar { @@ -441,3 +343,131 @@ html, body { .chat-box .bubble p { margin: 0; } + +/* Typing notification bubble */ +.chat-box .typing { + display: inline-block; + border-radius: 0.5em; + width: 2em; + padding: 0.25em 0.35em; + vertical-align: middle; + text-align: center; + color: #aaa; +} + +.chat-box li.single .typing { + margin-top: 0.75em; + margin-bottom: 0.75em; +} + +.chat-box li.last .typing { + margin-top: 0.15em; + margin-bottom: 0.75em; +} + +.chat-box .timestamp { + font-size: 80%; + color: #777; + padding-left: 1.5em; + padding-top: 0.5em; +} + +/* END of talk bubbles */ + +/* Input fields and buttons */ +button { + display: inline-block; + margin: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + border: 1px solid transparent; + white-space: nowrap; + padding: 0.25em 1em; + font-size: 1.35rem; + border-radius: 2px; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25); +} + +button[disabled], +html input[disabled] { + cursor: default; + opacity: 0.65; + box-shadow: none; +} + +button.round { + border-radius: 50%; + width: 5rem; + height: 5rem; + line-height: 1; + outline: none; /* otherwise a square outline is shown */ + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25); + z-index: 2; +} +button.blue { + background-color: #2196f3; /* blue 500 */ + color: white; +} + +button:active { + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +input { + background: transparent; + border: none; + border-bottom: 1px solid #999; + width: 100%; + margin: 0; + padding: 0; + outline: none; + transition: border 0.2s ease-in-out, box-shadow: 0.2s ease-in-out; +} +input:focus { + border-bottom: 1px solid #2196f3; + box-shadow: inset 0 -1px 0 #2196f3; +} + +/* END of input and buttons */ + +/* Modal dialog */ +.overlay { + position: absolute; + display: flex; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + background-color: rgba(0,0,0,0.5); +} +.modal-dialog { + background-color: white; + margin: auto; +} +/* END of modal dialog */ + +/* Miscellaneous */ + +/* Badge with the number of unread messages */ +.badge { + background-color: #0097A7; + color: white; + border-radius: 1em; +} + +/* Color of buttons */ +.blue { + color: #2196f3; /* blue 500 */ +} + +/* Material icons resized from the default 24pt */ +.material-icons { + font-size: 2rem; +} +.material-icons.small { + font-size: 1.5rem; +} diff --git a/fonts/glyphicons-halflings-regular.eot b/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953..00000000 Binary files a/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.svg b/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb5490..00000000 --- a/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/glyphicons-halflings-regular.ttf b/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc60..00000000 Binary files a/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.woff b/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e612858..00000000 Binary files a/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.woff2 b/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54..00000000 Binary files a/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/index.html b/index.html index 1c09f9ec..f00b942a 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,11 @@ Tinode example: chat web application in react.js + + + + @@ -82,12 +87,12 @@ this.props.onLogin(this.state.login.trim(), this.state.password.trim()); }, render: function() { - var submitClassList = "btn btn-primary"; + var submitClassList = "blue"; if (this.props.disabled) { submitClassList += " disabled"; } return ( -
+

- +
); @@ -148,12 +153,12 @@ } }, render: function() { - var submitClassList = "btn btn-primary"; + var submitClassList = "blue"; if (this.props.disabled) { submitClassList += " disabled"; } return ( -
+

- +
); @@ -197,29 +202,21 @@ var avatar = null; if (this.props.avatar) { var src = this.props.avatar === true ? "img/avatar.svg" : this.props.avatar; - avatar = avatar + avatar = avatar } return ( - ); } }) @@ -227,14 +224,11 @@ SideNavbar.MenuStart = React.createClass({ render: function() { return ( -
  • -
      -
    • -
    • -
    -
  • +
    + person_add +   + settings +
    ); } }); @@ -242,7 +236,7 @@ SideNavbar.MenuCancel = React.createClass({ render: function() { return ( -
  • + close ); } }) @@ -250,7 +244,7 @@ SideNavbar.MenuSettings = React.createClass({ render: function() { return ( -
  • + settings ); } }) @@ -284,7 +278,7 @@ default:; }; return ( -
    +
    {errorText}
    +
    {errorText}
    ); } }); @@ -325,7 +319,7 @@ /* BEGIN Contact list (list of topics) */ - /* A form for initiating a new topic */ + /* A form for initiating a new topic var NewTopic = React.createClass({ render: function() { return ( @@ -333,12 +327,13 @@
    ); } }); + */ /* var contactsData = [ @@ -365,10 +360,12 @@ title = title.substring(0, 30) + "..."; } return ( -
  • - -
    - +
  • +
    + + ● +
    +
    {title}

    {this.props.comment}

    @@ -383,7 +380,7 @@ var showUnreadBadge = null; if (this.props.count > 0) { var count = this.props.count > 99 ? "99+" : this.props.count; - showUnreadBadge = {count}; + showUnreadBadge = {count}; } return ( {showUnreadBadge} @@ -391,27 +388,6 @@ } }); - /* Online/offline indicator */ - Contact.Online = React.createClass({ - render: function() { - var lastOnline = ''; - if (this.props.now) { - lastOnline = "online"; - } else if (this.props.when) { - lastOnline = this.props.when; - } else { - lastOnline = "offline"; - } - - return ( - - ● -  {lastOnline} - - ); - } - }); - /* ContactsView holds all contacts-related stuff */ var ContactsView = React.createClass({ getInitialState: function() { @@ -461,15 +437,11 @@ }, this); return ( -
    -
    - -
    -
      - {contactNodes} -
    -
    -
    +
    +
      + {contactNodes} +
    +
    ); } @@ -516,9 +488,15 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab var Message = React.createClass({ render: function() { var liClass = (this.props.response ? "left" : "right") + " " + this.props.sequence; + var colors = { + backgroundColor: materialColor[materialColorIndex[this.props.userIndex]]["100"], + borderColor: materialColor[materialColorIndex[this.props.userIndex]]["100"] + }; + var bubbleClass = (this.props.sequence === "single" || this.props.sequence === "last") ? + "bubble tip" : "bubble"; return (
  • -
    +

    {this.props.content} @@ -538,69 +516,23 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab var marker = null; if (this.props.received === Received.SENDING) { - marker = '\u231A'; // watch face - } else if (this.props.received === Received.SERVER || this.props.received === Received.CLIENT) { - marker = '\u2714'; // checkmark - if (this.props.received === Received.CLIENT) { - var style = {marginLeft: "-0.5em"}; - marker += {'\u2714'}; - } + marker = (access_time); // watch face + } else if (this.props.received === Received.SERVER) { + marker = (done); // checkmark + } else if (this.props.received === Received.CLIENT) { + marker = (done_all); // double checkmark } else if (this.props.received === Received.READ) { - marker = '\uD83D\uDC41'; // open eye + marker = (done_all); // open eye } return ( - {timestamp}{'\u00a0'}{marker} + {timestamp}{'\u00a0'}{marker} ); } }); - /* Send message form */ - Message.Send = React.createClass({ - getInitialState:function() { - return { - message: '', - // Make initial keypress time as if it happened 5001 milliseconds in the past. - keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1 - }; - }, - handleSend: function(e) { - var message = this.state.message.trim(); - if (message) { - this.props.sendMessage(this.state.message.trim()); - this.setState({message: ''}); - } - }, - handleMessageTyping: function(e) { - var newState = {message: e.target.value}; - var now = new Date().getTime(); - if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) { - var topic = Tinode.getTopic(this.props.topic); - if (topic.isSubscribed()) { - topic.noteKeyPress(); - } - newState.keypressTimestamp = now; - } - this.setState(newState); - }, - render: function() { - return ( -

    - - - - - -
    - ); - } - }); - var MessagesView = React.createClass({ getInitialState: function() { return { @@ -615,8 +547,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab //if (node) { // node.scrollTop = node.scrollHeight; //} - var node = this.refs.messagesPanel; - node.scrollTop = node.scrollHeight; + //var node = this.refs.messagesPanel; + //node.scrollTop = node.scrollHeight; }, componentWillMount: function() { this.propsChange(this.props); @@ -673,53 +605,119 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab this.props.onData(msg); }, render: function() { - var me = Tinode.getCurrentUserID(); - var topic = Tinode.getTopic(this.state.topic); - var previousFrom = null; - var messageNodes = this.state.messages.map(function(msg) { - var isReply = !(msg.from === me); - var msgReceived = Received.UNDEF; - if (!isReply) { - if (topic.msgReadCount(msg.seq) > 1) { - msgReceived = Received.READ; - } else if (topic.msgRecvCount(msg.seq) > 1) { - msgReceived = Received.CLIENT; - } else { - msgReceived = Received.SERVER; + var component = null; + if (this.state.topic) { + var messageNodes = []; + var me = Tinode.getCurrentUserID(); + var topic = Tinode.getTopic(this.state.topic); + var previousFrom = null; + for (var i=0; i 1) { + msgReceived = Received.READ; + } else if (topic.msgRecvCount(msg.seq) > 1) { + msgReceived = Received.CLIENT; + } else { + msgReceived = Received.SERVER; + } + } + messageNodes.push( + + ); } - return ( - + component = ( +
    +
    +

    {this.state.title}

    +
    +
    +
      + {messageNodes} +
    +
    +
    + +
    +
    ); - }); + } else { + component = ( + + ); + } + return component; + } + }); + + /* Send message form */ + MessagesView.Send = React.createClass({ + getInitialState:function() { + return { + message: '', + // Make initial keypress time as if it happened 5001 milliseconds in the past. + keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1 + }; + }, + handleSend: function(e) { + var message = this.state.message.trim(); + if (message) { + this.props.sendMessage(this.state.message.trim()); + this.setState({message: ''}); + } + }, + handleMessageTyping: function(e) { + var newState = {message: e.target.value}; + var now = new Date().getTime(); + if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) { + var topic = Tinode.getTopic(this.props.topic); + if (topic.isSubscribed()) { + topic.noteKeyPress(); + } + newState.keypressTimestamp = now; + } + this.setState(newState); + }, + render: function() { return ( -
    -
    -

    {this.state.title}

    -
    -
    -
      - {messageNodes} -
    -
    -
    - -
    + ); } }); - /* This is just a static page to show when no conversation is selected */ - var DummyMessagesView = React.createClass({ + /* This is just a static page to display when no conversation is selected. */ + MessagesView.Dummy = React.createClass({ render: function() { return ( -
    - + ); } @@ -824,9 +822,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab }, render: function() { return ( -
    +
    -
    -
    -
    -
    -
    -
    - {this.state.topicSelected ? - : - } -
    -
    -
    -
    +
    ); @@ -865,10 +853,10 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab );