out of call chat

Initial implementation. Now clicking on an item in the Conversations
view (RecentModel) will bring up a chat view. In the case of a Person
(contact) it will select the chat with the last used ContactMethod of
that person. If there is more than one ContactMethod, as combo box will
be displayed giving the choice of ContactMethods to use.

To make a call, double-click the item as before. Any call (incoming or
outgoing) will superseed the chat view.

Out of call chats use the account based chat API. In call chats still
use the call based chat API.

Change-Id: I3deb09fd22c3dda7b78ea9be0eef32a6f27adecb
Tuleap: #203
diff --git a/src/chatview.cpp b/src/chatview.cpp
index efd74dd..0e4a0c5 100644
--- a/src/chatview.cpp
+++ b/src/chatview.cpp
@@ -47,8 +47,17 @@
     GtkWidget *button_chat_input;
     GtkWidget *entry_chat_input;
     GtkWidget *scrolledwindow_chat;
+    GtkWidget *hbox_chat_info;
+    GtkWidget *label_peer;
+    GtkWidget *combobox_cm;
 
-    Call *call;
+    /* only one of the three following pointers should be non void;
+     * either this is an in-call chat (and so the in-call chat APIs will be used)
+     * or it is an out of call chat (and so the account chat APIs will be used)
+     */
+    Call          *call;
+    Person        *person;
+    ContactMethod *cm;
 
     QMetaObject::Connection new_message_connection;
 };
@@ -90,7 +99,27 @@
     if (text && strlen(text) > 0) {
         QMap<QString, QString> messages;
         messages["text/plain"] = text;
-        priv->call->addOutgoingMedia<Media::Text>()->send(messages);
+
+        if (priv->call) {
+            // in call message
+            priv->call->addOutgoingMedia<Media::Text>()->send(messages);
+        } else if (priv->person) {
+            // get the chosen cm
+            auto active = gtk_combo_box_get_active(GTK_COMBO_BOX(priv->combobox_cm));
+            if (active >= 0) {
+                auto cm = priv->person->phoneNumbers().at(active);
+                if (!cm->sendOfflineTextMessage(messages))
+                    g_warning("message failed to send"); // TODO: warn the user about this in the UI
+            } else {
+                g_warning("no ContactMethod chosen; message not esnt");
+            }
+        } else if (priv->cm) {
+            if (!priv->cm->sendOfflineTextMessage(messages))
+                g_warning("message failed to send"); // TODO: warn the user about this in the UI
+        } else {
+            g_warning("no Call, Person, or ContactMethod set; message not sent");
+        }
+
         /* clear the entry */
         gtk_entry_set_text(GTK_ENTRY(priv->entry_chat_input), "");
     }
@@ -132,6 +161,9 @@
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_chat_input);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, entry_chat_input);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, scrolledwindow_chat);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, hbox_chat_info);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, label_peer);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, combobox_cm);
 
     chat_view_signals[NEW_MESSAGES_DISPLAYED] = g_signal_new (
         "new-messages-displayed",
@@ -217,9 +249,96 @@
     );
 }
 
-GtkWidget *
-chat_view_new(Call *call)
+static void
+selected_cm_changed(GtkComboBox *box, ChatView *self)
 {
+    g_return_if_fail(IS_CHAT_VIEW(self));
+    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
+
+    auto cms = priv->person->phoneNumbers();
+    auto active = gtk_combo_box_get_active(box);
+    if (active >= 0 && active < cms.size()) {
+        parse_chat_model(cms.at(active)->textRecording()->instantMessagingModel(), self);
+    } else {
+        g_warning("no valid ContactMethod selected to display chat conversation");
+    }
+}
+
+static void
+update_contact_methods(ChatView *self)
+{
+    g_return_if_fail(IS_CHAT_VIEW(self));
+    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->person);
+
+    /* model for the combobox for the choice of ContactMethods */
+    auto cm_model = gtk_list_store_new(
+        2, G_TYPE_STRING, G_TYPE_POINTER
+    );
+
+    auto cms = priv->person->phoneNumbers();
+    for (int i = 0; i < cms.size(); ++i) {
+        GtkTreeIter iter;
+        gtk_list_store_append(cm_model, &iter);
+        gtk_list_store_set(cm_model, &iter,
+                           0, cms.at(i)->uri().toUtf8().constData(),
+                           1, cms.at(i),
+                           -1);
+    }
+
+    gtk_combo_box_set_model(GTK_COMBO_BOX(priv->combobox_cm), GTK_TREE_MODEL(cm_model));
+    g_object_unref(cm_model);
+
+    auto renderer = gtk_cell_renderer_text_new();
+    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(priv->combobox_cm), renderer, FALSE);
+    gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(priv->combobox_cm), renderer, "text", 0, NULL);
+
+    /* select the last used cm */
+    if (!cms.isEmpty()) {
+        auto last_used_cm = cms.at(0);
+        int last_used_cm_idx = 0;
+        for (int i = 1; i < cms.size(); ++i) {
+            auto new_cm = cms.at(i);
+            if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0) {
+                last_used_cm = new_cm;
+                last_used_cm_idx = i;
+            }
+        }
+
+        gtk_combo_box_set_active(GTK_COMBO_BOX(priv->combobox_cm), last_used_cm_idx);
+    }
+
+    /* show the combo box if there is more than one cm to choose from */
+    if (cms.size() > 1)
+        gtk_widget_show_all(priv->combobox_cm);
+    else
+        gtk_widget_hide(priv->combobox_cm);
+}
+
+static void
+update_name(ChatView *self)
+{
+    g_return_if_fail(IS_CHAT_VIEW(self));
+    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->person || priv->cm);
+
+    QString name;
+    if (priv->person) {
+        name = priv->person->roleData(static_cast<int>(Ring::Role::Name)).toString();
+    } else {
+        name = priv->cm->roleData(static_cast<int>(Ring::Role::Name)).toString();
+    }
+    gtk_label_set_text(GTK_LABEL(priv->label_peer), name.toUtf8().constData());
+}
+
+GtkWidget *
+chat_view_new_call(Call *call)
+{
+    g_return_val_if_fail(call, nullptr);
+
     ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));
     ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
 
@@ -229,3 +348,41 @@
 
     return (GtkWidget *)self;
 }
+
+GtkWidget *
+chat_view_new_cm(ContactMethod *cm)
+{
+    g_return_val_if_fail(cm, nullptr);
+
+    ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));
+    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
+
+    priv->cm = cm;
+    parse_chat_model(priv->cm->textRecording()->instantMessagingModel(), self);
+    update_name(self);
+
+    gtk_widget_show(priv->hbox_chat_info);
+
+    return (GtkWidget *)self;
+}
+
+GtkWidget *
+chat_view_new_person(Person *p)
+{
+    g_return_val_if_fail(p, nullptr);
+
+    ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));
+    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
+
+    priv->person = p;
+
+    /* connect to the changed signal before setting the cm combo box, so that the correct
+     * conversation will get displayed */
+    g_signal_connect(priv->combobox_cm, "changed", G_CALLBACK(selected_cm_changed), self);
+    update_contact_methods(self);
+    update_name(self);
+
+    gtk_widget_show(priv->hbox_chat_info);
+
+    return (GtkWidget *)self;
+}
diff --git a/src/chatview.h b/src/chatview.h
index 90affe7..0710601 100644
--- a/src/chatview.h
+++ b/src/chatview.h
@@ -23,6 +23,8 @@
 #include <gtk/gtk.h>
 
 class Call;
+class ContactMethod;
+class Person;
 
 G_BEGIN_DECLS
 
@@ -36,8 +38,10 @@
 typedef struct _ChatViewClass ChatViewClass;
 
 
-GType      chat_view_get_type (void) G_GNUC_CONST;
-GtkWidget *chat_view_new      (Call* call);
+GType      chat_view_get_type   (void) G_GNUC_CONST;
+GtkWidget *chat_view_new_call   (Call* call);
+GtkWidget *chat_view_new_cm     (ContactMethod* cm);
+GtkWidget *chat_view_new_person (Person* p);
 
 G_END_DECLS
 
diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp
index bc6e4d4..61aae91 100644
--- a/src/currentcallview.cpp
+++ b/src/currentcallview.cpp
@@ -637,7 +637,7 @@
     }
 
     /* init chat view */
-    auto chat_view = chat_view_new(priv->call);
+    auto chat_view = chat_view_new_call(priv->call);
     gtk_container_add(GTK_CONTAINER(priv->frame_chat), chat_view);
 
     /* check if there were any chat notifications and open the chat view if so */
diff --git a/src/ringmainwindow.cpp b/src/ringmainwindow.cpp
index 48a492c..e6af40b 100644
--- a/src/ringmainwindow.cpp
+++ b/src/ringmainwindow.cpp
@@ -65,6 +65,7 @@
 #include "ringwelcomeview.h"
 #include "recentcontactsview.h"
 #include <recentmodel.h>
+#include "chatview.h"
 
 static constexpr const char* CALL_VIEW_NAME             = "calls";
 static constexpr const char* CREATE_ACCOUNT_1_VIEW_NAME = "create1";
@@ -189,12 +190,13 @@
  * This takes the RecentModel index as the argument and displays the corresponding view:
  * - incoming call view
  * - current call view
- * TODO: chat view
+ * - chat view
  * - welcome view (if no index is selected)
  */
 static void
-selection_changed(const QModelIndex& idx, RingMainWindow *win)
+selection_changed(const QModelIndex& recent_idx, RingMainWindow *win)
 {
+    // g_debug("selection changed");
     g_return_if_fail(IS_RING_MAIN_WINDOW(win));
     RingMainWindowPrivate *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
 
@@ -208,12 +210,17 @@
     /* make sure we leave full screen, since the call selection is changing */
     leave_full_screen(win);
 
-    /* show the call if there is one associated with this index */
-    auto call_idx = CallModel::instance().getIndex(RecentModel::instance().getActiveCall(idx));
+    /* check which object type is selected */
+    auto type = recent_idx.data(static_cast<int>(Ring::Role::ObjectType)).value<Ring::ObjectType>();
+    auto object = recent_idx.data(static_cast<int>(Ring::Role::Object));
+    /* try to get the call model index, in case its a call, since we're still using the CallModel as well */
+    auto call_idx = CallModel::instance().getIndex(RecentModel::instance().getActiveCall(recent_idx));
+
+    /* we prioritize showing the call view */
     if (call_idx.isValid()) {
+        /* show the call view */
         QVariant state =  call_idx.data(static_cast<int>(Call::Role::LifeCycleState));
         GtkWidget *new_call_view = NULL;
-        char* new_call_view_name = NULL;
 
         switch(state.value<Call::LifeCycleState>()) {
             case Call::LifeCycleState::CREATION:
@@ -221,15 +228,11 @@
             case Call::LifeCycleState::FINISHED:
                 new_call_view = incoming_call_view_new();
                 incoming_call_view_set_call_info(INCOMING_CALL_VIEW(new_call_view), call_idx);
-                /* use the pointer of the call as a unique name */
-                new_call_view_name = g_strdup_printf("%p_incoming", (void *)CallModel::instance().getCall(call_idx));
                 break;
             case Call::LifeCycleState::PROGRESS:
                 new_call_view = current_call_view_new();
                 g_signal_connect(new_call_view, "video-double-clicked", G_CALLBACK(video_double_clicked), win);
                 current_call_view_set_call_info(CURRENT_CALL_VIEW(new_call_view), call_idx);
-                /* use the pointer of the call as a unique name */
-                new_call_view_name = g_strdup_printf("%p_current", (void *)CallModel::instance().getCall(call_idx));
                 break;
             case Call::LifeCycleState::COUNT__:
                 g_warning("LifeCycleState should never be COUNT");
@@ -239,9 +242,20 @@
         gtk_container_remove(GTK_CONTAINER(priv->frame_call), old_call_view);
         gtk_container_add(GTK_CONTAINER(priv->frame_call), new_call_view);
         gtk_widget_show(new_call_view);
-        g_free(new_call_view_name);
+    } else if (type == Ring::ObjectType::Person && object.isValid()) {
+        /* show chat view constructed from Person object */
+        auto new_chat_view = chat_view_new_person(object.value<Person *>());
+        gtk_container_remove(GTK_CONTAINER(priv->frame_call), old_call_view);
+        gtk_container_add(GTK_CONTAINER(priv->frame_call), new_chat_view);
+        gtk_widget_show(new_chat_view);
+    } else if (type == Ring::ObjectType::ContactMethod && object.isValid()) {
+        /* show chat view constructed from CM */
+        auto new_chat_view = chat_view_new_cm(object.value<ContactMethod *>());
+        gtk_container_remove(GTK_CONTAINER(priv->frame_call), old_call_view);
+        gtk_container_add(GTK_CONTAINER(priv->frame_call), new_chat_view);
+        gtk_widget_show(new_chat_view);
     } else {
-        /* nothing selected in the call model, so show the default screen */
+        /* nothing selected that we can display, show the welcome view */
         gtk_container_remove(GTK_CONTAINER(priv->frame_call), old_call_view);
         gtk_container_add(GTK_CONTAINER(priv->frame_call), priv->welcome_view);
     }
@@ -250,56 +264,81 @@
 static void
 call_state_changed(Call *call, RingMainWindow *win)
 {
-    // g_debug("call state changed")   ;
+    // g_debug("call state changed");
     RingMainWindowPrivate *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
 
     /* if we're showing the settings, then nothing needs to be done as the call
        view is not shown */
     if (priv->show_settings) return;
 
-    /* check if the call that changed state is the same as the selected call */
-    QModelIndex idx_selected = CallModel::instance().selectionModel()->currentIndex();
+    /* we prioritize showing the call view; but if the call is over we go back to showing the chat view */
 
-    if( idx_selected.isValid() && call == CallModel::instance().getCall(idx_selected)) {
-        // g_debug("selected call state changed");
+    /* check if the call that changed state is the same as the selected call */
+    auto idx_selected = RecentModel::instance().selectionModel()->currentIndex();
+
+    if(call == RecentModel::instance().getActiveCall(idx_selected)) {
         /* check if we need to change the view */
-        GtkWidget *old_call_view = gtk_bin_get_child(GTK_BIN(priv->frame_call));
-        GtkWidget *new_call_view = NULL;
+        auto current_view = gtk_bin_get_child(GTK_BIN(priv->frame_call));
         QVariant state = CallModel::instance().data(idx_selected, static_cast<int>(Call::Role::LifeCycleState));
 
         /* check what the current state is vs what is displayed */
         switch(state.value<Call::LifeCycleState>()) {
             case Call::LifeCycleState::CREATION:
+            case Call::LifeCycleState::FINISHED:
+            /* go back to incoming call view;
+             * it will show that the call failed and offer to hang it up */
             case Call::LifeCycleState::INITIALIZATION:
-                /* LifeCycleState cannot go backwards, so it should not be possible
-                 * that the call is displayed as current (meaning that its in progress)
-                 * but have the state 'initialization' */
-                if (IS_CURRENT_CALL_VIEW(old_call_view))
-                    g_warning("call displayed as current, but is in state of initialization");
-                break;
-            case Call::LifeCycleState::PROGRESS:
-                if (IS_INCOMING_CALL_VIEW(old_call_view)) {
-                    /* change from incoming to current */
-                    new_call_view = current_call_view_new();
-                    current_call_view_set_call_info(CURRENT_CALL_VIEW(new_call_view), idx_selected);
-                    /* use the pointer of the call as a unique name */
-                    char* new_call_view_name = NULL;
-                    new_call_view_name = g_strdup_printf("%p_current", (void *)CallModel::instance().getCall(idx_selected));
-                    g_free(new_call_view_name);
-                    gtk_container_remove(GTK_CONTAINER(priv->frame_call), old_call_view);
-                    gtk_container_add(GTK_CONTAINER(priv->frame_call), new_call_view);
-                    gtk_widget_show(new_call_view);
-                    g_signal_connect(new_call_view, "video-double-clicked", G_CALLBACK(video_double_clicked), win);
+                {
+                    /* show the incoming call view */
+                    if (!IS_INCOMING_CALL_VIEW(current_view)) {
+                        auto new_view = incoming_call_view_new();
+                        incoming_call_view_set_call_info(INCOMING_CALL_VIEW(new_view), CallModel::instance().getIndex(call));
+                        gtk_container_remove(GTK_CONTAINER(priv->frame_call), current_view);
+                        gtk_container_add(GTK_CONTAINER(priv->frame_call), new_view);
+                        gtk_widget_show(new_view);
+                    }
                 }
                 break;
-            case Call::LifeCycleState::FINISHED:
-                /* leave fullscreen if call is over */
-                leave_full_screen(win);
+            case Call::LifeCycleState::PROGRESS:
+                {
+                    /* show the current call view */
+                    if (!IS_CURRENT_CALL_VIEW(current_view)) {
+                        auto new_view = current_call_view_new();
+                        g_signal_connect(new_view, "video-double-clicked", G_CALLBACK(video_double_clicked), win);
+                        current_call_view_set_call_info(CURRENT_CALL_VIEW(new_view), CallModel::instance().getIndex(call));
+                        gtk_container_remove(GTK_CONTAINER(priv->frame_call), current_view);
+                        gtk_container_add(GTK_CONTAINER(priv->frame_call), new_view);
+                        gtk_widget_show(new_view);
+                    }
+                }
                 break;
             case Call::LifeCycleState::COUNT__:
                 g_warning("LifeCycleState should never be COUNT");
                 break;
         }
+    } else if (idx_selected.isValid()) {
+        /* otherwise, the call is over and is already removed from the RecentModel */
+        auto current_view = gtk_bin_get_child(GTK_BIN(priv->frame_call));
+        leave_full_screen(win);
+
+        /* show the chat view */
+        if (!IS_CHAT_VIEW(current_view)) {
+            auto type = idx_selected.data(static_cast<int>(Ring::Role::ObjectType)).value<Ring::ObjectType>();
+            auto object = idx_selected.data(static_cast<int>(Ring::Role::Object));
+            if (type == Ring::ObjectType::Person && object.isValid()) {
+                /* show chat view constructed from Person object */
+                auto new_view = chat_view_new_person(object.value<Person *>());
+                gtk_container_remove(GTK_CONTAINER(priv->frame_call), current_view);
+                gtk_container_add(GTK_CONTAINER(priv->frame_call), new_view);
+                gtk_widget_show(new_view);
+            } else if (type == Ring::ObjectType::ContactMethod && object.isValid()) {
+                /* show chat view constructed from CM */
+                auto new_view = chat_view_new_cm(object.value<ContactMethod *>());
+                gtk_container_remove(GTK_CONTAINER(priv->frame_call), current_view);
+                gtk_container_add(GTK_CONTAINER(priv->frame_call), new_view);
+                gtk_widget_show(new_view);
+            }
+        }
     }
 }
 
@@ -910,7 +949,16 @@
     QObject::connect(
         &CallModel::instance(),
         &CallModel::callStateChanged,
-        [=](Call* call, G_GNUC_UNUSED Call::State previousState) {
+        [win](Call* call, G_GNUC_UNUSED Call::State previousState) {
+            call_state_changed(call, win);
+        }
+    );
+
+    /* also connect to the incoming call, in case the RecentModel item we already selected gets a call */
+    QObject::connect(
+        &CallModel::instance(),
+        &CallModel::incomingCall,
+        [win](Call* call) {
             call_state_changed(call, win);
         }
     );
diff --git a/ui/chatview.ui b/ui/chatview.ui
index 87f4973..a425d47 100644
--- a/ui/chatview.ui
+++ b/ui/chatview.ui
@@ -3,7 +3,48 @@
   <requires lib="gtk+" version="3.10"/>
   <template class="ChatView" parent="GtkBox">
     <property name="orientation">vertical</property>
-    <property name="spacing">5</property>
+
+    <!-- chat info (only show for out of call conversations) -->
+    <child>
+      <object class="GtkBox" id="hbox_chat_info">
+        <property name="visible">False</property>
+        <property name="no-show-all">True</property>
+        <property name="orientation">horizontal</property>
+        <property name="spacing">5</property>
+        <property name="border-width">5</property>
+        <child>
+          <object class="GtkLabel" id="label_peer">
+            <property name="visible">True</property>
+            <property name="selectable">True</property>
+            <property name="ellipsize">end</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkComboBox" id="combobox_cm">
+            <property name="visible">False</property>
+            <property name="popup-fixed-width">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+      </packing>
+    </child>
+    <!-- end of chat info -->
+
     <!-- start of chat text view -->
     <child>
       <object class="GtkScrolledWindow" id="scrolledwindow_chat">
@@ -26,12 +67,12 @@
       </packing>
     </child>
     <!-- end of chat text view -->
+
     <!-- start of chat entry -->
     <child>
       <object class="GtkBox" id="hbox_chat_input">
         <property name="visible">True</property>
         <property name="orientation">horizontal</property>
-        <property name="spacing">5</property>
         <child>
           <object class="GtkEntry" id="entry_chat_input">
             <property name="visible">True</property>
@@ -53,5 +94,7 @@
         <property name="fill">True</property>
       </packing>
     </child>
+    <!-- end of chat entry -->
+
   </template>
 </interface>