Commit c6a3b98a authored by Stepan Salenikovich's avatar Stepan Salenikovich

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
parent d2cad06c
......@@ -47,8 +47,17 @@ struct _ChatViewPrivate
GtkWidget *button_chat_input;
GtkWidget *entry_chat_input;
GtkWidget *scrolledwindow_chat;
Call *call;
GtkWidget *hbox_chat_info;
GtkWidget *label_peer;
GtkWidget *combobox_cm;
/* 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 @@ send_chat(G_GNUC_UNUSED GtkWidget *widget, ChatView *self)
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 @@ chat_view_class_init(ChatViewClass *klass)
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 @@ parse_chat_model(QAbstractItemModel *model, ChatView *self)
);
}
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)
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 @@ chat_view_new(Call *call)
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;
}
......@@ -23,6 +23,8 @@
#include <gtk/gtk.h>
class Call;
class ContactMethod;
class Person;
G_BEGIN_DECLS
......@@ -36,8 +38,10 @@ typedef struct _ChatView ChatView;
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
......
......@@ -637,7 +637,7 @@ current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
}
/* 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 */
......
......@@ -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 @@ video_double_clicked(G_GNUC_UNUSED CurrentCallView *view, RingMainWindow *self)
* 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 @@ selection_changed(const QModelIndex& idx, RingMainWindow *win)
/* 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 @@ selection_changed(const QModelIndex& idx, RingMainWindow *win)
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 @@ selection_changed(const QModelIndex& idx, RingMainWindow *win)
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 @@ selection_changed(const QModelIndex& idx, RingMainWindow *win)
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;
/* we prioritize showing the call view; but if the call is over we go back to showing the chat view */
/* check if the call that changed state is the same as the selected call */
QModelIndex idx_selected = CallModel::instance().selectionModel()->currentIndex();
auto idx_selected = RecentModel::instance().selectionModel()->currentIndex();
if( idx_selected.isValid() && call == CallModel::instance().getCall(idx_selected)) {
// g_debug("selected call state changed");
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");
{
/* 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::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 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::FINISHED:
/* leave fullscreen if call is over */
leave_full_screen(win);
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 @@ ring_main_window_init(RingMainWindow *win)
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);
}
);
......
......@@ -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>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment