Commit a51e9c08 authored by Hugo Lefeuvre's avatar Hugo Lefeuvre

call management: implement let a message view

Change-Id: I3157d745c460fd554d663031db8a170f153e5f55
Reviewed-by: Sébastien Blin's avatarSebastien Blin <sebastien.blin@savoirfairelinux.com>
parent 03eb4e6b
......@@ -308,6 +308,8 @@ SET( SRC_FILES
src/webkitchatcontainer.h
src/webkitchatcontainer.cpp
src/chatview.h
src/messagingwidget.h
src/messagingwidget.cpp
src/chatview.cpp
src/avatarmanipulation.h
src/avatarmanipulation.cpp
......
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg3398"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="baseline-mic-24px.svg">
<metadata
id="metadata3408">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3406" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1855"
inkscape:window-height="1176"
id="namedview3404"
showgrid="false"
inkscape:snap-grids="true"
inkscape:zoom="5.4114583"
inkscape:cx="-1.4303771"
inkscape:cy="8.3244998"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg3398" />
<path
d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"
id="path3400" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path3402" />
<circle
style="fill:#dc2719"
cx="17.333334"
cy="17.666666"
r="3.3333333"
id="circle3476" />
<ellipse
style="fill:none;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none"
id="path4305"
cx="-8.3156881"
cy="-1.3628489"
rx="3.9268527"
ry="6.0981712" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M6 6h12v12H6z"/>
</svg>
......@@ -12,6 +12,8 @@
<file alias="call_start">ic_video_call_black_24px.svg</file>
<file alias="call_end">ic_call_end_white_24px.svg</file>
<file alias="record">ic_fiber_manual_record_red_24px.svg</file>
<file alias="record_audio">baseline-mic-24px.svg</file>
<file alias="recording_audio">baseline-mic-recording-24px.svg</file>
<file alias="contact">contact.svg</file>
<file alias="accept">accept.svg</file>
<file alias="chat">ic_chat_white_24px.svg</file>
......@@ -20,6 +22,8 @@
<file alias="unmute_video">ic_videocam_off_white_24px.svg</file>
<file alias="mute_video">ic_videocam_white_24px.svg</file>
<file alias="pause">ic_pause_white_24px.svg</file>
<file alias="stop">baseline-stop-24px.svg</file>
<file alias="send">baseline-send-24px.svg</file>
<file alias="play">ic_play_arrow_white_24px.svg</file>
<file alias="quality">ic_high_quality_white_24px.svg</file>
<file alias="contacts_list">ic_people_black_24px.svg</file>
......@@ -28,6 +32,7 @@
<file alias="history_list">ic_history_black_24px.svg</file>
<file alias="add">add.svg</file>
<file alias="reject">reject.svg</file>
<file alias="quit">baseline-exit_to_app-24px.svg</file>
<file alias="block">block.svg</file>
<file alias="block_black">block_black.svg</file>
<file alias="invite">ic_person_add_black_24px.svg</file>
......
......@@ -17,4 +17,15 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
namespace lrc
{
namespace api
{
namespace account
{
struct Info;
}
}
}
typedef const lrc::api::account::Info* AccountInfoPointer;
......@@ -3,6 +3,7 @@
* Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
* Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
* Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -21,11 +22,19 @@
#include "incomingcallview.h"
// Gtk
/* std */
#include <stdexcept>
// GTK+ related
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
// Qt
#include <QSize>
// Lrc
#include <api/avmodel.h>
#include <api/newcallmodel.h>
#include <api/contactmodel.h>
#include <api/conversationmodel.h>
......@@ -34,6 +43,7 @@
// Client
#include "chatview.h"
#include "messagingwidget.h"
#include "native/pixbufmanipulator.h"
#include "utils/drawing.h"
#include "utils/files.h"
......@@ -61,6 +71,8 @@ struct _IncomingCallViewPrivate
GtkWidget *button_accept_incoming;
GtkWidget *button_reject_incoming;
GtkWidget *frame_chat;
GtkWidget *box_messaging_widget;
GtkWidget *messaging_widget;
// The webkit_chat_container is created once, then reused for all chat views
GtkWidget *webkit_chat_container;
......@@ -101,6 +113,7 @@ incoming_call_view_dispose(GObject *object)
static gboolean
map_boolean_to_orientation(GValue *value, GVariant *variant, G_GNUC_UNUSED gpointer user_data)
{
bool ret = FALSE;
if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) {
if (g_variant_get_boolean(variant)) {
// true, chat should be horizontal (to the right)
......@@ -109,9 +122,9 @@ map_boolean_to_orientation(GValue *value, GVariant *variant, G_GNUC_UNUSED gpoin
// false, chat should be vertical (at the bottom)
g_value_set_enum(value, GTK_ORIENTATION_VERTICAL);
}
return TRUE;
ret = TRUE;
}
return FALSE;
return ret;
}
static void
......@@ -127,8 +140,9 @@ accept_incoming_call(G_GNUC_UNUSED GtkWidget *widget, IncomingCallView *self)
{
g_return_if_fail(IS_INCOMING_CALL_VIEW(self));
auto priv = INCOMING_CALL_VIEW_GET_PRIVATE(self);
auto contactUri = priv->conversation_->participants[0];
try {
auto contactUri = priv->conversation_->participants.at(0);
auto contact = (*priv->accountInfo_)->contactModel->getContact(contactUri);
// If the contact is pending, we should accept its request
if (contact.profileInfo.type == lrc::api::profile::Type::PENDING)
......@@ -140,6 +154,14 @@ accept_incoming_call(G_GNUC_UNUSED GtkWidget *widget, IncomingCallView *self)
}
}
static void
on_leave_action(IncomingCallView *view)
{
g_return_if_fail(IS_INCOMING_CALL_VIEW(view));
auto priv = INCOMING_CALL_VIEW_GET_PRIVATE(view);
(*priv->accountInfo_)->conversationModel->selectConversation(priv->conversation_->uid);
}
static void
incoming_call_view_init(IncomingCallView *view)
{
......@@ -154,6 +176,7 @@ incoming_call_view_init(IncomingCallView *view)
.green-button:hover { background: #219d55; }",
-1, nullptr
);
gtk_style_context_add_provider_for_screen(gdk_display_get_default_screen(gdk_display_get_default()),
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
......@@ -186,9 +209,10 @@ incoming_call_view_class_init(IncomingCallViewClass *klass)
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, label_bestId);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, spinner_status);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, label_status);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, button_accept_incoming);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, button_reject_incoming);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, button_accept_incoming);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, frame_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), IncomingCallView, box_messaging_widget);
}
static void
......@@ -202,17 +226,21 @@ update_state(IncomingCallView *view)
if (!(*priv->accountInfo_)->callModel->hasCall(callId)) return;
auto call = (*priv->accountInfo_)->callModel->getCall(callId);
gchar *status = g_strdup_printf("%s", lrc::api::call::to_string(call.status).c_str());
gtk_label_set_text(GTK_LABEL(priv->label_status), status);
g_free(status);
gtk_label_set_text(GTK_LABEL(priv->label_status), lrc::api::call::to_string(call.status).c_str());
if (call.status == lrc::api::call::Status::INCOMING_RINGING)
gtk_widget_show(priv->button_accept_incoming);
else
gtk_widget_hide(priv->button_accept_incoming);
gtk_widget_show(priv->button_reject_incoming);
gtk_widget_show(priv->spinner_status);
if (call.status != lrc::api::call::Status::PEER_BUSY &&
call.status != lrc::api::call::Status::ENDED) {
gtk_widget_hide(priv->messaging_widget);
gtk_widget_show(priv->label_status);
gtk_widget_show(priv->spinner_status);
gtk_widget_show(priv->button_reject_incoming);
gtk_widget_show(priv->spinner_status);
}
}
static void
......@@ -275,6 +303,7 @@ set_call_info(IncomingCallView *view) {
GtkWidget *
incoming_call_view_new(WebKitChatContainer* view,
lrc::api::AVModel& avModel,
AccountInfoPointer const & accountInfo,
lrc::api::conversation::Info* conversation)
{
......@@ -285,6 +314,10 @@ incoming_call_view_new(WebKitChatContainer* view,
priv->conversation_ = conversation;
priv->accountInfo_ = &accountInfo;
priv->messaging_widget = messaging_widget_new(avModel, conversation, accountInfo);
gtk_box_pack_start(GTK_BOX(priv->box_messaging_widget), priv->messaging_widget, TRUE, TRUE, 0);
g_signal_connect_swapped(priv->messaging_widget, "leave-action", G_CALLBACK(on_leave_action), self);
set_call_info(INCOMING_CALL_VIEW(self));
return GTK_WIDGET(self);
......@@ -298,3 +331,18 @@ incoming_call_view_get_conversation(IncomingCallView *self)
return *priv->conversation_;
}
void
incoming_call_view_let_a_message(IncomingCallView* view, lrc::api::conversation::Info conv)
{
g_return_if_fail(IS_INCOMING_CALL_VIEW(view));
auto priv = INCOMING_CALL_VIEW_GET_PRIVATE(view);
g_return_if_fail(priv->conversation_->uid == conv.uid);
gtk_widget_hide(priv->label_status);
gtk_widget_hide(priv->spinner_status);
gtk_widget_hide(priv->button_accept_incoming);
gtk_widget_hide(priv->button_reject_incoming);
gtk_widget_show(priv->messaging_widget);
}
......@@ -33,6 +33,7 @@ namespace lrc
{
namespace api
{
class AVModel;
namespace conversation
{
struct Info;
......@@ -54,8 +55,10 @@ typedef struct _IncomingCallViewClass IncomingCallViewClass;
GType incoming_call_view_get_type (void) G_GNUC_CONST;
GtkWidget *incoming_call_view_new (WebKitChatContainer* view,
lrc::api::AVModel& avModel,
AccountInfoPointer const & accountInfo,
lrc::api::conversation::Info* conversation);
void incoming_call_view_let_a_message(IncomingCallView* view, lrc::api::conversation::Info conv);
lrc::api::conversation::Info incoming_call_view_get_conversation (IncomingCallView*);
G_END_DECLS
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
* Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "messagingwidget.h"
// std
#include <string>
#include <sstream>
#include <iomanip> // for std::put_time
#include <chrono>
// GTK+ related
#include <gtk/gtk.h>
#include <glib/gi18n.h>
// Lrc
#include <api/avmodel.h>
#include <api/account.h>
#include <api/conversationmodel.h>
namespace { namespace details
{
class CppImpl;
}}
struct _MessagingWidget
{
GtkVBox parent;
};
struct _MessagingWidgetClass
{
GtkVBoxClass parent_class;
};
typedef struct _MessagingWidgetPrivate MessagingWidgetPrivate;
struct _MessagingWidgetPrivate
{
GtkWidget *label_letmessage;
GtkWidget *label_duration;
GtkWidget *image_send;
GtkWidget *image_stop;
GtkWidget *box_red_dot;
GtkWidget *box_timer;
GtkWidget *image_record_audio;
GtkWidget *button_record_audio;
GtkWidget *button_end_without_message;
details::CppImpl* cpp; ///< Non-UI and C++ only code
};
/* signals */
enum {
LEAVE_ACTION,
LAST_SIGNAL
};
static guint selfie_signals[LAST_SIGNAL] = { 0 };
G_DEFINE_TYPE_WITH_PRIVATE(MessagingWidget, messaging_widget, GTK_TYPE_VBOX);
#define MESSAGING_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), MESSAGING_WIDGET_TYPE, MessagingWidgetPrivate))
namespace { namespace details
{
class CppImpl
{
public:
explicit CppImpl(MessagingWidget& widget);
~CppImpl();
void init();
void setup(lrc::api::AVModel& avModel,
lrc::api::conversation::Info* conversation,
AccountInfoPointer const & accountInfo);
void set_state(MessagingWidgetState newState);
MessagingWidget* self = nullptr; // The GTK widget itself
MessagingWidgetPrivate* widgets = nullptr;
// store current recording location
std::string saveFileName_;
// internal state: used to keep track of current layout
MessagingWidgetState state_;
// timer related
std::chrono::seconds timeElapsed_ {0};
static const std::chrono::seconds timeLimit;
guint timerCallbackId_ {0};
lrc::api::AVModel* avModel_;
lrc::api::conversation::Info* conversation_;
AccountInfoPointer const * accountInfo_;
};
const std::chrono::seconds CppImpl::timeLimit = std::chrono::seconds(30);
static gboolean
on_timer_update(gpointer user_data)
{
MessagingWidget *self = MESSAGING_WIDGET(user_data);
auto priv = MESSAGING_WIDGET_GET_PRIVATE(self);
gboolean ret = TRUE;
if (priv->cpp->state_ != MESSAGING_WIDGET_REC_AUDIO) {
// state changed, stop timer
ret = FALSE;
} else {
priv->cpp->timeElapsed_++;
}
std::time_t elapsed = priv->cpp->timeElapsed_.count();
std::stringstream ss;
ss << std::put_time(std::localtime(&elapsed), "%M:%S");
gtk_label_set_text(GTK_LABEL(priv->label_duration), ss.str().c_str());
return ret;
}
static void
on_leave_widget(MessagingWidget *self) {
g_return_if_fail(IS_MESSAGING_WIDGET(self));
auto priv = MESSAGING_WIDGET_GET_PRIVATE(self);
// no need to stop recording here, will be done in the destructor
g_signal_emit(self, selfie_signals[LEAVE_ACTION], 0);
}
static void
on_record_button_pressed(MessagingWidget *self)
{
g_return_if_fail(IS_MESSAGING_WIDGET(self));
auto priv = MESSAGING_WIDGET_GET_PRIVATE(self);
switch(priv->cpp->state_) {
case MESSAGING_WIDGET_STATE_INIT:
priv->cpp->set_state(MESSAGING_WIDGET_REC_AUDIO);
break;
case MESSAGING_WIDGET_REC_AUDIO:
priv->cpp->set_state(MESSAGING_WIDGET_AUDIO_REC_SUCCESS);
break;
case MESSAGING_WIDGET_AUDIO_REC_SUCCESS:
priv->cpp->set_state(MESSAGING_WIDGET_REC_SENT);
break;
default:
priv->cpp->set_state(MESSAGING_WIDGET_STATE_INIT);
g_error("on_record_button_pressed: invalid state");
}
}
CppImpl::CppImpl(MessagingWidget& widget)
: self {&widget}
, widgets {MESSAGING_WIDGET_GET_PRIVATE(&widget)}
{}
CppImpl::~CppImpl()
{
if (state_ == MESSAGING_WIDGET_REC_AUDIO) {
// stop current recording
set_state(MESSAGING_WIDGET_AUDIO_REC_SUCCESS);
}
}
void
CppImpl::init()
{
// CSS styles
auto provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(provider,
".flat-button { border: 0; border-radius: 50%; transition: all 0.3s ease; } \
.grey-button { background: #dfdfdf; } \
.grey-button:hover { background: #cecece; } \
.time-label { padding: 5px; } \
.timer-box { border: solid 2px; border-radius: 6px; }",
-1, nullptr
);
gtk_style_context_add_provider_for_screen(gdk_display_get_default_screen(gdk_display_get_default()),
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
// signals
g_signal_connect_swapped(widgets->button_record_audio, "clicked", G_CALLBACK(on_record_button_pressed), self);
g_signal_connect_swapped(widgets->button_end_without_message, "clicked", G_CALLBACK(on_leave_widget), self);
set_state(MESSAGING_WIDGET_STATE_INIT);
}
void
CppImpl::setup(lrc::api::AVModel& avModel,
lrc::api::conversation::Info* conversation,
AccountInfoPointer const & accountInfo)
{
conversation_ = conversation;
accountInfo_ = &accountInfo;
avModel_ = &avModel;
}
void
CppImpl::set_state(MessagingWidgetState state)
{
// state transition validity checking is expected to be done by the caller
switch (state) {
case MESSAGING_WIDGET_STATE_INIT:
gtk_label_set_text(GTK_LABEL(widgets->label_letmessage), _("Contact appears to be busy. Leave a message ?"));
gtk_widget_show(widgets->label_letmessage);
gtk_widget_show(widgets->button_record_audio);
gtk_widget_set_sensitive(widgets->button_record_audio, true);
gtk_widget_show(widgets->button_end_without_message);
gtk_widget_set_sensitive(widgets->button_end_without_message, true);
break;
case MESSAGING_WIDGET_REC_AUDIO:
{
std::string file_name = avModel_->startLocalRecorder(true);
if (file_name.empty()) {
g_warning("set_state: failed to start recording");
return;
}
saveFileName_ = file_name;
timerCallbackId_ = g_timeout_add_seconds(1, on_timer_update, self);
gtk_widget_show(widgets->box_timer);
gtk_widget_show(widgets->label_duration);
gtk_widget_show(widgets->box_red_dot);
gtk_button_set_image(GTK_BUTTON(widgets->button_record_audio), widgets->image_stop);
gtk_widget_set_tooltip_text(GTK_WIDGET(widgets->button_record_audio), _("Stop recording"));
break;
}
case MESSAGING_WIDGET_AUDIO_REC_SUCCESS:
if (!saveFileName_.empty()) {
avModel_->stopLocalRecorder(saveFileName_);
}
gtk_widget_hide(widgets->box_red_dot);
if (timerCallbackId_) {
g_source_remove(timerCallbackId_);
}
gtk_button_set_image(GTK_BUTTON(widgets->button_record_audio), widgets->image_send);
gtk_widget_set_tooltip_text(GTK_WIDGET(widgets->button_record_audio), _("Send recorded message"));
break;
case MESSAGING_WIDGET_REC_SENT:
if (auto model = (*accountInfo_)->conversationModel.get()) {
model->sendFile(conversation_->uid, saveFileName_, g_path_get_basename(saveFileName_.c_str()));
saveFileName_ = "";
}
on_leave_widget(self);
break;
default:
g_debug("set_state: invalid state.");
return;
}
state_ = state;
if (state_ == MESSAGING_WIDGET_AUDIO_REC_SUCCESS) {
on_timer_update(self);
}
}
}} // namespace details
static void
messaging_widget_dispose(GObject *object)
{
MessagingWidgetPrivate *priv = MESSAGING_WIDGET_GET_PRIVATE(object);
delete priv->cpp;
priv->cpp = nullptr;
G_OBJECT_CLASS(messaging_widget_parent_class)->dispose(object);
}
static void
messaging_widget_finalize(GObject *object)
{
G_OBJECT_CLASS(messaging_widget_parent_class)->finalize(object);
}
static void
messaging_widget_class_init(MessagingWidgetClass *klass)
{
G_OBJECT_CLASS(klass)->finalize = messaging_widget_finalize;
G_OBJECT_CLASS(klass)->dispose = messaging_widget_dispose;
gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass), "/cx/ring/RingGnome/messagingwidget.ui");