gnome: add text message support

Refs #74038

Change-Id: I98b4443b9dc5a8975a63d12ee3bd71f68d4b1452
diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp
index 82c8ebd..18df982 100644
--- a/src/currentcallview.cpp
+++ b/src/currentcallview.cpp
@@ -39,6 +39,10 @@
 #include <contactmethod.h>
 #include <person.h>
 #include "delegates/pixbufdelegate.h"
+#include <media/media.h>
+#include <media/text.h>
+#include <media/textrecording.h>
+#include "models/gtkqtreemodel.h"
 
 struct _CurrentCallView
 {
@@ -60,12 +64,21 @@
     GtkWidget *label_duration;
     GtkWidget *frame_video;
     GtkWidget *video_widget;
-    GtkWidget *button_hangup;
+    GtkWidget *revealer_chat;
+    GtkWidget *togglebutton_chat;
+    GtkWidget *textview_chat;
+    GtkWidget *button_chat_input;
+    GtkWidget *entry_chat_input;
+    GtkWidget *scrolledwindow_chat;
+
+    Call *call;
 
     QMetaObject::Connection state_change_connection;
     QMetaObject::Connection call_details_connection;
     QMetaObject::Connection local_renderer_connection;
     QMetaObject::Connection remote_renderer_connection;
+    QMetaObject::Connection media_added_connection;
+    QMetaObject::Connection new_message_connection;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE(CurrentCallView, current_call_view, GTK_TYPE_BOX);
@@ -85,14 +98,65 @@
     QObject::disconnect(priv->call_details_connection);
     QObject::disconnect(priv->local_renderer_connection);
     QObject::disconnect(priv->remote_renderer_connection);
+    QObject::disconnect(priv->media_added_connection);
+    QObject::disconnect(priv->new_message_connection);
 
     G_OBJECT_CLASS(current_call_view_parent_class)->dispose(object);
 }
 
 static void
+chat_toggled(GtkToggleButton *togglebutton, CurrentCallView *self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    gtk_revealer_set_reveal_child(GTK_REVEALER(priv->revealer_chat),
+                                  gtk_toggle_button_get_active(togglebutton));
+
+    if (gtk_toggle_button_get_active(togglebutton)) {
+        /* create an outgoing media to bring up chat history, if any */
+        priv->call->addOutgoingMedia<Media::Text>();
+    }
+}
+
+static void
+send_chat(G_GNUC_UNUSED GtkWidget *widget, CurrentCallView *self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    /* make sure there is text to send */
+    const gchar *text = gtk_entry_get_text(GTK_ENTRY(priv->entry_chat_input));
+    if (text && strlen(text) > 0) {
+        priv->call->addOutgoingMedia<Media::Text>()->send(text);
+        /* clear the entry */
+        gtk_entry_set_text(GTK_ENTRY(priv->entry_chat_input), "");
+    }
+}
+
+static void
+scroll_to_bottom(GtkAdjustment *adjustment, G_GNUC_UNUSED gpointer user_data)
+{
+    gtk_adjustment_set_value(adjustment,
+        gtk_adjustment_get_upper(adjustment) - gtk_adjustment_get_page_size(adjustment));
+}
+
+static void
 current_call_view_init(CurrentCallView *view)
 {
     gtk_widget_init_template(GTK_WIDGET(view));
+
+    CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
+
+    g_signal_connect(priv->togglebutton_chat, "toggled", G_CALLBACK(chat_toggled), view);
+    g_signal_connect(priv->button_chat_input, "clicked", G_CALLBACK(send_chat), view);
+    g_signal_connect(priv->entry_chat_input, "activate", G_CALLBACK(send_chat), view);
+
+    /* the adjustment params will change only when the model is created and when
+     * new messages are added; in these cases we want to scroll to the bottom of
+     * the chat treeview */
+    GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(priv->scrolledwindow_chat));
+    g_signal_connect(adjustment, "changed", G_CALLBACK(scroll_to_bottom), NULL);
 }
 
 static void
@@ -108,7 +172,12 @@
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_status);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_duration);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_video);
-    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_hangup);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, revealer_chat);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_chat);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, textview_chat);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_chat_input);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, entry_chat_input);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scrolledwindow_chat);
 }
 
 GtkWidget *
@@ -207,15 +276,64 @@
     return TRUE;
 }
 
+static void
+print_message_to_buffer(const QModelIndex &idx, GtkTextBuffer *buffer)
+{
+    if (idx.isValid()) {
+        QVariant message = idx.data();
+
+        gchar *text = g_strdup_printf("%s\n", message.value<QString>().toUtf8().constData());
+
+        GtkTextIter iter;
+        gtk_text_buffer_get_end_iter(buffer, &iter);
+        gtk_text_buffer_insert(buffer, &iter, text, -1);
+
+        g_free(text);
+    } else {
+        g_warning("QModelIndex in im model is not valid");
+    }
+}
+
+static void
+parse_chat_model(QAbstractItemModel *model, CurrentCallView *self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    /* new model, disconnect from the old model updates and clear the text buffer */
+    QObject::disconnect(priv->new_message_connection);
+
+    GtkTextBuffer *new_buffer = gtk_text_buffer_new(NULL);
+    gtk_text_view_set_buffer(GTK_TEXT_VIEW(priv->textview_chat), new_buffer);
+    g_object_unref(new_buffer);
+
+    /* put all the messages in the im model into the text view */
+    for (int row = 0; row < model->rowCount(); ++row) {
+        QModelIndex idx = model->index(row, 0);
+        print_message_to_buffer(idx, new_buffer);
+    }
+
+    /* append new messages */
+    priv->new_message_connection = QObject::connect(
+        model,
+        &QAbstractItemModel::rowsInserted,
+        [priv, model] (const QModelIndex &parent, int first, int last) {
+            for (int row = first; row <= last; ++row) {
+                QModelIndex idx = model->index(row, 0, parent);
+                print_message_to_buffer(idx, gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview_chat)));
+            }
+        }
+    );
+}
 
 void
 current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
     CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
 
-    Call *call = CallModel::instance()->getCall(idx);
+    priv->call = CallModel::instance()->getCall(idx);
 
     /* get call image */
-    QVariant var_i = PixbufDelegate::instance()->callPhoto(call, QSize(60, 60), false);
+    QVariant var_i = PixbufDelegate::instance()->callPhoto(priv->call, QSize(60, 60), false);
     std::shared_ptr<GdkPixbuf> image = var_i.value<std::shared_ptr<GdkPixbuf>>();
     gtk_image_set_from_pixbuf(GTK_IMAGE(priv->image_peer), image.get());
 
@@ -225,19 +343,19 @@
     gtk_label_set_text(GTK_LABEL(priv->label_identity), ba_name.constData());
 
     /* change some things depending on call state */
-    update_state(view, call);
-    update_details(view, call);
+    update_state(view, priv->call);
+    update_details(view, priv->call);
 
     priv->state_change_connection = QObject::connect(
-        call,
+        priv->call,
         &Call::stateChanged,
-        [=]() { update_state(view, call); }
+        [view, priv]() { update_state(view, priv->call); }
     );
 
     priv->call_details_connection = QObject::connect(
-        call,
+        priv->call,
         static_cast<void (Call::*)(void)>(&Call::changed),
-        [=]() { update_details(view, call); }
+        [view, priv]() { update_details(view, priv->call); }
     );
 
     /* video widget */
@@ -247,14 +365,14 @@
 
     /* check if we already have a renderer */
     video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
-                                   call->videoRenderer(),
+                                   priv->call->videoRenderer(),
                                    VIDEO_RENDERER_REMOTE);
 
     /* callback for remote renderer */
     priv->remote_renderer_connection = QObject::connect(
-        call,
+        priv->call,
         &Call::videoStarted,
-        [=](Video::Renderer *renderer) {
+        [priv](Video::Renderer *renderer) {
             video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
                                            renderer,
                                            VIDEO_RENDERER_REMOTE);
@@ -271,7 +389,7 @@
     priv->local_renderer_connection = QObject::connect(
         Video::PreviewManager::instance(),
         &Video::PreviewManager::previewStarted,
-        [=](Video::Renderer *renderer) {
+        [priv](Video::Renderer *renderer) {
             video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
                                            renderer,
                                            VIDEO_RENDERER_LOCAL);
@@ -282,4 +400,25 @@
     g_signal_connect(priv->video_widget, "button-press-event",
                      G_CALLBACK(on_button_press_in_video_event),
                      view);
+
+    /* check if text media is already present */
+    if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) {
+        Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::IN);
+        parse_chat_model(text->recording()->instantMessagingModel(), view);
+    } else if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) {
+        Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::OUT);
+        parse_chat_model(text->recording()->instantMessagingModel(), view);
+    } else {
+        /* monitor media for messaging text messaging */
+        priv->media_added_connection = QObject::connect(
+            priv->call,
+            &Call::mediaAdded,
+            [view, priv] (Media::Media* media) {
+                if (media->type() == Media::Media::Type::TEXT) {
+                    parse_chat_model(((Media::Text*)media)->recording()->instantMessagingModel(), view);
+                    QObject::disconnect(priv->media_added_connection);
+                }
+            }
+        );
+    }
 }
diff --git a/ui/currentcallview.ui b/ui/currentcallview.ui
index 1a99224..7b9c8d1 100644
--- a/ui/currentcallview.ui
+++ b/ui/currentcallview.ui
@@ -60,8 +60,84 @@
             <property name="visible">False</property>
             <property name="shadow-type">GTK_SHADOW_NONE</property>
           </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+          </packing>
         </child>
         <!-- end of video widget -->
+        <!-- start of text message area -->
+        <child>
+          <object class="GtkRevealer" id="revealer_chat">
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkBox" id="vbox_chat">
+                <property name="visible">True</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">5</property>
+                <property name="margin-start">5</property>
+                <!-- start of chat text view -->
+                <child>
+                  <object class="GtkScrolledWindow" id="scrolledwindow_chat">
+                    <property name="visible">True</property>
+                    <property name="min-content-height">150</property>
+                    <child>
+                      <object class="GtkTextView" id="textview_chat">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="editable">False</property>
+                        <property name="wrap-mode">word-char</property>
+                        <property name="left-margin">5</property>
+                        <property name="right-margin">5</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                  </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">0</property>
+                    <style>
+                      <class name="linked"/>
+                    </style>
+                    <child>
+                      <object class="GtkEntry" id="entry_chat_input">
+                        <property name="visible">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="button_chat_input">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">Send</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                  </packing>
+                </child>
+                <!-- end of chat entry -->
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+          </packing>
+        </child>
+        <!-- end of text message area -->
         <!-- call status box -->
         <child>
           <object class="GtkBox" id="hbox_call_status">
@@ -109,83 +185,106 @@
 
     <!-- box which contains the control buttons -->
     <child>
-      <object class="GtkBox" id="hbox_call_controls">
+      <object class="GtkBox" id="hbox_controls">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="halign">center</property>
-        <property name="valign">start</property>
         <property name="border_width">10</property>
-        <style>
-          <class name="linked"/>
-        </style>
+        <!-- call conrol button box -->
         <child>
-          <object class="GtkButton" id="button_hangup">
-            <property name="label" translatable="yes">Hang-up</property>
+          <object class="GtkButtonBox" id="buttonbox_call_controls">
             <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="action-name">app.hangup</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkButton" id="button_hangup">
+                <property name="label" translatable="yes">Hang-up</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action-name">app.hangup</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="togglebutton_hold">
+                <property name="label" translatable="yes">Hold</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action-name">app.hold</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="togglebutton_muteaudio">
+                <property name="label" translatable="yes">Mute audio</property>
+                <!-- TODO: set visible when feature enabled -->
+                <property name="visible">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action-name">app.mute_audio</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="togglebutton_mutevideo">
+                <property name="label" translatable="yes">Mute video</property>
+                <!-- TODO: set visible when feature enabled -->
+                <property name="visible">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action-name">app.mute_video</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="togglebutton_record">
+                <property name="label" translatable="yes">Record</property>
+                <!-- TODO: set visible when feature enabled -->
+                <property name="visible">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action-name">app.record</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="togglebutton_chat">
+                <property name="label" translatable="yes">Chat</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
           </object>
           <packing>
-            <property name="expand">False</property>
+            <property name="expand">True</property>
             <property name="fill">True</property>
           </packing>
         </child>
-        <child>
-          <object class="GtkToggleButton" id="togglebutton_hold">
-            <property name="label" translatable="yes">Hold</property>
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="action-name">app.hold</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkToggleButton" id="togglebutton_muteaudio">
-            <property name="label" translatable="yes">Mute audio</property>
-            <!-- TODO: set visible when feature enabled -->
-            <property name="visible">False</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="action-name">app.mute_audio</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkToggleButton" id="togglebutton_mutevideo">
-            <property name="label" translatable="yes">Mute video</property>
-            <!-- TODO: set visible when feature enabled -->
-            <property name="visible">False</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="action-name">app.mute_video</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkToggleButton" id="togglebutton_record">
-            <property name="label" translatable="yes">Record</property>
-            <!-- TODO: set visible when feature enabled -->
-            <property name="visible">False</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="action-name">app.record</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
+        <!-- end of call control button box -->
       </object>
       <packing>
         <property name="expand">False</property>