From 86775b88e4b8afc3a044ed2d1947ed249eb1c33b Mon Sep 17 00:00:00 2001
From: Drew Fisher <drew.m.fisher@gmail.com>
Date: Sat, 17 May 2014 00:20:15 -0700
Subject: [PATCH] frontend: many things

- Allow switching between tabs with click
- Sending messages works and appends to chat log
- Make IMs received propagate to conversation tabs
---
 reactornado/dispatcher.js |   7 +--
 reactornado/widgets.js    | 125 ++++++++++++++++++++++++++++++++------
 2 files changed, 107 insertions(+), 25 deletions(-)

diff --git a/reactornado/dispatcher.js b/reactornado/dispatcher.js
index 4667757..b22b0d6 100644
--- a/reactornado/dispatcher.js
+++ b/reactornado/dispatcher.js
@@ -53,12 +53,7 @@ var Dispatcher = {
         }
 
         if (method == "recv_im") {
-            window.conversations[bid].visible = true;
-            window.conversations[bid].messages.push({
-                message: params.message,
-                timestamp: params.timestamp,
-                sender: "them"
-            });
+            window.reactor.refs.conversations.handleRecvIm(params);
         } else if (method == "buddy_typing_state") {
             window.conversations[bid].type_state = params.state;
         }
diff --git a/reactornado/widgets.js b/reactornado/widgets.js
index a1dafc4..cb3e325 100644
--- a/reactornado/widgets.js
+++ b/reactornado/widgets.js
@@ -89,7 +89,8 @@ var ConversationTab = React.createClass({
         if (this.props.active) {
             classname = classname + " conv-active";
         }
-        return <li className={classname}>{this.props.buddy}</li>;
+        var label = this.props.buddy.display_name;
+        return <li className={classname} onClick={this.props.onClick}>{this.props.buddy.display_name}</li>;
     }
 });
 
@@ -97,7 +98,12 @@ var ConversationTabBar = React.createClass({
     render: function() {
         var tabs = [];
         for (var i = 0 ; i < this.props.tabs.length ; i++) {
-            tabs.push(ConversationTab(this.props.tabs[i]));
+            tabs.push(ConversationTab({
+                key: "tab" + i,
+                onClick: this.props.onTabSwitch.bind(null, i),
+                buddy: this.props.tabs[i],
+                active: (this.props.focused_tab == i)
+            }));
         }
         return <ul className="conv-tabbar">{ tabs }</ul>;
     }
@@ -112,7 +118,7 @@ var ConversationTextEntry = React.createClass({
         if (e && e.keyCode == 13) {
             // TODO: do this only if Shift not currently pressed? (awkward...)
             e.preventDefault();
-            // TODO: do something with this.state.text - window.Dispatcher.send_im?
+            this.props.onSendMessage(this.state.text);
             this.setState(this.getInitialState());
         }
         // TODO: close window when Escape pressed?
@@ -128,36 +134,117 @@ var ConversationTabContent = React.createClass({
         var messages = [];
         for (var i = 0 ; i < this.props.messages.length ; i++) {
             var msg = this.props.messages[i];
+            var sender = ((msg.direction == "outgoing") ? "me" : this.props.buddy.display_name) + ":";
+            var senderclass = "sender " + msg.direction;
             var msgclass = "msg " + msg.direction;
-            messages.push(<p>
+            var msgkey = "msg" + i;
+            messages.push(<p key={msgkey}>
                 <span className="timestamp">{msg.timestamp}</span>
-                <span className="sender">{msg.sender}</span>
+                <span className={senderclass}>{sender}</span>
                 <span className={msgclass}>{msg.text}</span>
                 </p>);
         }
         return <div className="conv-tab-content">
                 <div className="conv-log">{ messages }</div>
-                <ConversationTextEntry />
+                <ConversationTextEntry onSendMessage={this.props.onSendMessage}/>
                </div>;
     }
 });
 
 var Conversations = React.createClass({
+    getInitialState: function() {
+        return {
+            focused_tab: -1,  // this is zero-indexed, so -1 means "no tab"
+            conversations: []
+        };
+    },
+    handleTabSwitch: function(focusedTabIndex) {
+        console.log("Switching to tab " + focusedTabIndex);
+        this.setState({ focused_tab: focusedTabIndex,
+                        conversations: this.state.conversations
+        });
+    },
+    handleSendMessage: function(message) {
+        // 1) Dispatch IM to buddy
+        var active_convs = this.state.conversations.filter(function(obj) { return obj.visible; });
+        var conv = active_convs[this.state.focused_tab];
+        var contact = window.blist[conv.buddy.id];
+        var params = {
+            proto: contact.proto,
+            account: contact.account,
+            buddy: contact.buddy,
+            message: message
+        };
+        console.log("sending im:", params);
+        window.Dispatcher.send_msg("conversations", "send_im", params);
+        // 2) Append IM to sent messages.  Later, I might want to hold off on this until I get the ack
+        //    from the server, but for now, this will give the most responsive UI.
+        var now = new Date();
+        // TODO: extract this datetime-formatting thing to some other module
+        // TODO: zero-pad these for values < 10
+        var timestamp = "" + now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds();
+        conv.messages.push({
+            direction: "outgoing",
+            timestamp: timestamp,
+            text: message
+        });
+        // It's awkward trying to deep copy the entire messages array.  I am a
+        // bad person and am mutating this.state directly, so when done
+        // changing state, call forceUpdate()
+        this.forceUpdate();
+    },
+    getConversation: function(proto, account, buddy) {
+        // Helper function to get a particular entry in the conversations list.
+        // If a conversation already exists, it finds it.
+        // Otherwise, it creates a new one for the specified buddy.
+        var bid = proto + ":" + account + ":" + buddy;
+        for (var i = 0 ; i < this.state.conversations.length ; i++) {
+            if (this.state.conversations[i].buddy.id == bid) {
+                return this.state.conversations[i];
+            }
+        }
+        // None found, construct a new one.
+        var entry = {
+                visible: true,
+                buddy: {id: bid, display_name: buddy},
+                messages: []
+        };
+        this.state.conversations.push(entry);
+        this.forceUpdate();
+        return entry;
+    },
+    handleRecvIm: function(params) {
+        var bid = params.proto + ":" + params.account + ":" + params.buddy;
+        if (this.state.conversations[bid] === undefined) {
+            this.state.conversations[bid] = {
+                visible: true,
+                buddy: {id: bid, display_name: params.buddy},
+                messages: []
+            };
+        }
+        this.getConversation(params.proto, params.account, params.buddy).messages.push({
+            direction: "incoming",
+            timestamp: params.timestamp,
+            text: params.message
+        });
+        this.forceUpdate();
+    },
     render: function() {
-        // TODO: pull data from this.state and provide a method to handle
-        // dispatcher-sent messages
-        var conv_tabs = [ {"active": true, "buddy": "Drew Fisher"},
-                          {"active": false, "buddy": "Kate Harrison"},
-                          {"active": false, "buddy": "Matt Mullins"}
-               ];
-        var active_pane_messages = [ {"direction": "incoming",
-                               "sender": "nobody",
-                               "timestamp": "10:41",
-                               "text": "this is a test message"
-                              } ];
+        var active_convs = this.state.conversations.filter(function(obj) { return obj.visible; });
+
+        // conv_tabs is just an Array of strings with the tab title
+        var conv_tabs = active_convs.map(function(obj) { return obj.buddy; });
+
+        var active_pane_messages = [];
+        var active_buddy = null;
+        if (this.state.focused_tab >= 0 && this.state.focused_tab < active_convs.length) {
+            active_pane_messages = active_convs[this.state.focused_tab].messages;
+            active_buddy = active_convs[this.state.focused_tab].buddy;
+        }
+
         return <div className="conversations">
-                <ConversationTabBar tabs={conv_tabs} />
-                <ConversationTabContent messages={active_pane_messages} />
+                <ConversationTabBar onTabSwitch={this.handleTabSwitch} focused_tab={this.state.focused_tab} tabs={conv_tabs} />
+                <ConversationTabContent onSendMessage={this.handleSendMessage} buddy={active_buddy} messages={active_pane_messages} />
                </div>;
     }
 });
-- 
2.39.5