Commit 24fbf117 authored by Sébastien Blin's avatar Sébastien Blin

chatview: add video recorder

Change-Id: Ia71fa51bd2d490ba5b710ce2b77f489eb7f466f6
parent 9fbc163f
......@@ -23,7 +23,9 @@
<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="stop-white">stop-white.svg</file>
<file alias="send">baseline-send-24px.svg</file>
<file alias="send-white">send-white.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>
......@@ -52,5 +54,6 @@
<file alias="bottom_arrow">bottom_arrow.svg</file>
<file alias="up_arrow">up_arrow.svg</file>
<file alias="qrcode">qrcode.svg</file>
<file alias="retry">retry.svg</file>
</gresource>
</gresources>
<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="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" fill="#ffffff"/></svg>
\ No newline at end of file
<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" fill="white"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="white"/>
<path d="M6 6h12v12H6z" fill="none"/>
</svg>
......@@ -29,25 +29,36 @@
// GTK
#include <glib/gi18n.h>
#include <clutter-gtk/clutter-gtk.h>
// Qt
#include <QSize>
// LRC
#include <api/call.h>
#include <api/contactmodel.h>
#include <api/conversationmodel.h>
#include <api/contact.h>
#include <api/lrc.h>
#include <api/newcallmodel.h>
#include <api/call.h>
#include <api/avmodel.h>
// Client
#include "marshals.h"
#include "utils/files.h"
#include "native/pixbufmanipulator.h"
#include "video/video_widget.h"
/* size of avatar */
static constexpr int AVATAR_WIDTH = 150; /* px */
static constexpr int AVATAR_HEIGHT = 150; /* px */
enum class RecordAction {
RECORD,
STOP,
SEND
};
struct CppImpl {
struct Interaction {
std::string conv;
......@@ -55,6 +66,11 @@ struct CppImpl {
lrc::api::interaction::Info info;
};
std::vector<Interaction> interactionsBuffer_;
lrc::api::AVModel* avModel_;
RecordAction current_action_ {RecordAction::RECORD};
// store current recording location
std::string saveFileName_;
};
struct _ChatView
......@@ -83,13 +99,25 @@ struct _ChatViewPrivate
QMetaObject::Connection interaction_removed;
QMetaObject::Connection update_interaction_connection;
QMetaObject::Connection update_add_to_conversations;
QMetaObject::Connection local_renderer_connection;
gulong webkit_ready;
gulong webkit_send_text;
gulong webkit_drag_drop;
bool ready_ {false};
CppImpl* cpp_;
bool readyToRecord_ {false};
CppImpl* cpp;
bool video_started_by_settings;
GtkWidget* video_widget;
GtkWidget* record_popover;
GtkWidget* button_retry;
GtkWidget* button_main_action;
GtkWidget* label_time;
guint timer_duration = 0;
uint32_t duration = 0;
};
G_DEFINE_TYPE_WITH_PRIVATE(ChatView, chat_view, GTK_TYPE_BOX);
......@@ -108,6 +136,9 @@ enum {
static guint chat_view_signals[LAST_SIGNAL] = { 0 };
static void
init_video_widget(ChatView* self);
static void
chat_view_dispose(GObject *object)
{
......@@ -121,6 +152,7 @@ chat_view_dispose(GObject *object)
QObject::disconnect(priv->update_interaction_connection);
QObject::disconnect(priv->interaction_removed);
QObject::disconnect(priv->update_add_to_conversations);
QObject::disconnect(priv->local_renderer_connection);
/* Destroying the box will also destroy its children, and we wouldn't
* want that. So we remove the webkit_chat_container from the box. */
......@@ -140,6 +172,14 @@ chat_view_dispose(GObject *object)
priv->webkit_chat_container = nullptr;
}
if (priv->video_widget) {
gtk_widget_destroy(priv->video_widget);
}
if (priv->record_popover) {
gtk_widget_destroy(priv->record_popover);
}
G_OBJECT_CLASS(chat_view_parent_class)->dispose(object);
}
......@@ -223,6 +263,128 @@ file_to_manipulate(GtkWindow* top_window, bool send)
static void update_chatview_frame(ChatView *self);
void
on_record_closed(GtkPopover*, ChatView *self)
{
g_return_if_fail(IS_CHAT_VIEW(self));
auto* priv = CHAT_VIEW_GET_PRIVATE(self);
if (!priv->cpp->saveFileName_.empty()) {
priv->cpp->avModel_->stopLocalRecorder(priv->cpp->saveFileName_);
priv->cpp->saveFileName_ = "";
}
priv->cpp->current_action_ = RecordAction::RECORD;
if (priv->timer_duration) g_source_remove(priv->timer_duration);
priv->cpp->avModel_->stopPreview();
priv->duration = 0;
}
void
chat_view_show_video_recorder(ChatView *self, int pt_x, int pt_y)
{
g_return_if_fail(IS_CHAT_VIEW(self));
auto* priv = CHAT_VIEW_GET_PRIVATE(self);
if (!priv->readyToRecord_) return;
priv->cpp->avModel_->startPreview();
// CSS styles
auto provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(provider,
".record-button { background: rgba(0, 0, 0, 0.2); border-radius: 50%; border: 0; transition: all 0.3s ease; } \
.record-button:hover { background: rgba(0, 0, 0, 0.2); border-radius: 50%; border: 0; transition: all 0.3s ease; } \
.label_time { color: white; }",
-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);
auto deviceName = priv->cpp->avModel_->getDefaultDeviceName();
auto settings = priv->cpp->avModel_->getDeviceSettings(deviceName);
auto res = settings.size;
if (res.find("x") == std::string::npos) return;
auto width = static_cast<double>(std::stoi(res.substr(0, res.find("x"))));
auto height = static_cast<double>(std::stoi(res.substr(res.find("x") + 1)));
auto max = std::max(width, height);
#if GTK_CHECK_VERSION(3,22,0)
GdkRectangle workarea = {};
gdk_monitor_get_workarea(
gdk_display_get_primary_monitor(gdk_display_get_default()),
&workarea);
auto widget_size = std::max(300, workarea.width / 6);
#else
auto widget_size = std::max(300, gdk_screen_width() / 6);
#endif
width = width / max * widget_size;
height = height / max * widget_size;
if (priv->record_popover) {
gtk_widget_destroy(priv->record_popover);
}
init_video_widget(self);
priv->record_popover = gtk_popover_new(GTK_WIDGET(priv->box_webkit_chat_container));
g_signal_connect(priv->record_popover, "closed", G_CALLBACK(on_record_closed), self);
gtk_popover_set_relative_to(GTK_POPOVER(priv->record_popover), GTK_WIDGET(priv->box_webkit_chat_container));
gtk_container_add(GTK_CONTAINER(GTK_POPOVER(priv->record_popover)), priv->video_widget);
GdkRectangle rect;
rect.width = 1;
rect.height = 1;
rect.x = pt_x;
rect.y = pt_y;
gtk_popover_set_pointing_to(GTK_POPOVER(priv->record_popover), &rect);
gtk_widget_set_size_request(GTK_WIDGET(priv->video_widget), width, height);
#if GTK_CHECK_VERSION(3,22,0)
gtk_popover_popdown(GTK_POPOVER(priv->record_popover));
#endif
gtk_widget_show_all(priv->record_popover);
}
static gboolean
on_timer_duration_timeout(ChatView* view)
{
g_return_val_if_fail(IS_CHAT_VIEW(view), G_SOURCE_REMOVE);
auto* priv = CHAT_VIEW_GET_PRIVATE(view);
priv->duration += 1;
auto m = std::to_string(priv->duration / 60);
if (m.length() == 1) {
m = "0" + m;
}
auto s = std::to_string(priv->duration % 60);
if (s.length() == 1) {
s = "0" + s;
}
auto time_txt = m + ":" + s;
gtk_label_set_text(GTK_LABEL(priv->label_time), time_txt.c_str());
return G_SOURCE_CONTINUE;
}
static void
reset_recorder(ChatView *self)
{
g_return_if_fail(IS_CHAT_VIEW(self));
auto* priv = CHAT_VIEW_GET_PRIVATE(self);
gtk_widget_hide(GTK_WIDGET(priv->button_retry));
auto image = gtk_image_new_from_resource ("/net/jami/JamiGnome/stop-white");
gtk_button_set_image(GTK_BUTTON(priv->button_main_action), image);
priv->cpp->current_action_ = RecordAction::STOP;
gtk_label_set_text(GTK_LABEL(priv->label_time), "00:00");
priv->duration = 0;
priv->timer_duration = g_timeout_add(1000, (GSourceFunc)on_timer_duration_timeout, self);
std::string file_name = priv->cpp->avModel_->startLocalRecorder(false);
if (file_name.empty()) {
g_warning("set_state: failed to start recording");
return;
}
priv->cpp->saveFileName_ = file_name;
}
static void
webkit_chat_container_script_dialog(GtkWidget* webview, gchar *interaction, ChatView* self)
{
......@@ -334,6 +496,18 @@ webkit_chat_container_script_dialog(GtkWidget* webview, gchar *interaction, Chat
} catch (...) {
g_warning("delete interaction failed: can't find %s", order.substr(std::string("RETRY_INTERACTION:").size()).c_str());
}
} else if (order.find("VIDEO_RECORD:") == 0) {
auto pos_str {order.substr(std::string("VIDEO_RECORD:").size())};
auto sep_idx = pos_str.find("x");
if (sep_idx == std::string::npos)
return;
try {
int pt_x = stoi(pos_str.substr(0, sep_idx));
int pt_y = stoi(pos_str.substr(sep_idx + 1));
chat_view_show_video_recorder(self, pt_x, pt_y);
} catch (...) {
// ignore
}
}
}
......@@ -541,7 +715,7 @@ webkit_chat_container_ready(ChatView* self)
load_participants_images(self);
priv->ready_ = true;
for (const auto& interaction: priv->cpp_->interactionsBuffer_) {
for (const auto& interaction: priv->cpp->interactionsBuffer_) {
if (interaction.conv == priv->conversation_->uid) {
print_interaction_to_buffer(self, interaction.id, interaction.info);
}
......@@ -642,6 +816,7 @@ update_chatview_frame(ChatView* self)
}
}
} catch (const std::out_of_range&) {}
chat_view_set_record_visible(self, lrc::api::Lrc::activeCalls().size() == 0);
}
static void
......@@ -678,6 +853,118 @@ on_webkit_drag_drop(GtkWidget*, gchar* data, ChatView* self)
}
}
static void
on_main_action_clicked(ChatView *self)
{
g_return_if_fail(IS_CHAT_VIEW(self));
auto* priv = CHAT_VIEW_GET_PRIVATE(self);
switch (priv->cpp->current_action_) {
case RecordAction::RECORD: {
reset_recorder(self);
break;
}
case RecordAction::STOP: {
if (!priv->cpp->saveFileName_.empty()) {
priv->cpp->avModel_->stopLocalRecorder(priv->cpp->saveFileName_);
}
gtk_widget_show(GTK_WIDGET(priv->button_retry));
auto image = gtk_image_new_from_resource ("/net/jami/JamiGnome/send-white");
gtk_button_set_image(GTK_BUTTON(priv->button_main_action), image);
priv->cpp->current_action_ = RecordAction::SEND;
g_source_remove(priv->timer_duration);
break;
}
case RecordAction::SEND: {
if (auto model = (*priv->accountInfo_)->conversationModel.get()) {
model->sendFile(priv->conversation_->uid, priv->cpp->saveFileName_, g_path_get_basename(priv->cpp->saveFileName_.c_str()));
priv->cpp->saveFileName_ = "";
}
gtk_widget_destroy(priv->record_popover);
priv->cpp->current_action_ = RecordAction::RECORD;
priv->cpp->avModel_->stopPreview();
break;
}
}
}
static void
init_video_widget(ChatView* self)
{
ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
if (priv->video_widget && GTK_IS_WIDGET(priv->video_widget)) gtk_widget_destroy(priv->video_widget);
priv->video_widget = video_widget_new();
try {
const lrc::api::video::Renderer* previewRenderer =
&priv->cpp->avModel_->getRenderer(
lrc::api::video::PREVIEW_RENDERER_ID);
priv->video_started_by_settings = previewRenderer->isRendering();
if (priv->video_started_by_settings) {
video_widget_add_new_renderer(VIDEO_WIDGET(priv->video_widget),
priv->cpp->avModel_, previewRenderer, VIDEO_RENDERER_REMOTE);
} else {
priv->video_started_by_settings = true;
priv->local_renderer_connection = QObject::connect(
&*priv->cpp->avModel_,
&lrc::api::AVModel::rendererStarted,
[=](const std::string& id) {
if (id != lrc::api::video::PREVIEW_RENDERER_ID
|| !priv->readyToRecord_)
return;
video_widget_add_new_renderer(
VIDEO_WIDGET(priv->video_widget),
priv->cpp->avModel_,
previewRenderer, VIDEO_RENDERER_REMOTE);
});
}
} catch (const std::out_of_range& e) {
g_warning("Cannot start preview");
}
auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->video_widget));
auto* hbox_record_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 15);
auto image_retry = gtk_image_new_from_resource("/net/jami/JamiGnome/retry");
auto image_record = gtk_image_new_from_resource("/net/jami/JamiGnome/record");
priv->button_retry = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(priv->button_retry), GTK_RELIEF_NONE);
gtk_widget_set_tooltip_text(priv->button_retry, _("Retry"));
gtk_button_set_image(GTK_BUTTON(priv->button_retry), image_retry);
gtk_widget_set_size_request(GTK_WIDGET(priv->button_retry), 48, 48);
GtkStyleContext* context;
context = gtk_widget_get_style_context(GTK_WIDGET(priv->button_retry));
gtk_style_context_add_class(context, "record-button");
g_signal_connect_swapped(priv->button_retry, "clicked", G_CALLBACK(reset_recorder), self);
priv->button_main_action = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(priv->button_main_action), GTK_RELIEF_NONE);
gtk_widget_set_tooltip_text(priv->button_main_action, _("Record"));
gtk_button_set_image(GTK_BUTTON(priv->button_main_action), image_record);
gtk_widget_set_size_request(GTK_WIDGET(priv->button_main_action), 48, 48);
context = gtk_widget_get_style_context(GTK_WIDGET(priv->button_main_action));
gtk_style_context_add_class(context, "record-button");
g_signal_connect_swapped(priv->button_main_action, "clicked", G_CALLBACK(on_main_action_clicked), self);
priv->label_time = gtk_label_new("00:00");
context = gtk_widget_get_style_context(GTK_WIDGET(priv->label_time));
gtk_style_context_add_class(context, "label_time");
gtk_container_add(GTK_CONTAINER(hbox_record_controls), priv->button_retry);
gtk_container_add(GTK_CONTAINER(hbox_record_controls), priv->button_main_action);
gtk_container_add(GTK_CONTAINER(hbox_record_controls), priv->label_time);
gtk_widget_show_all(hbox_record_controls);
gtk_widget_hide(priv->button_retry);
auto actor_controls = gtk_clutter_actor_new_with_contents(hbox_record_controls);
clutter_actor_add_child(stage, actor_controls);
clutter_actor_set_x_align(actor_controls, CLUTTER_ACTOR_ALIGN_CENTER);
clutter_actor_set_y_align(actor_controls, CLUTTER_ACTOR_ALIGN_END);
}
static void
build_chat_view(ChatView* self)
{
......@@ -706,20 +993,19 @@ build_chat_view(ChatView* self)
G_CALLBACK(on_webkit_drag_drop),
self
);
priv->new_interaction_connection = QObject::connect(
&*(*priv->accountInfo_)->conversationModel, &lrc::api::ConversationModel::newInteraction,
[self, priv](const std::string& uid, uint64_t interactionId, lrc::api::interaction::Info interaction) {
if (!priv->conversation_) return;
if (!priv->ready_ && priv->cpp_) {
priv->cpp_->interactionsBuffer_.emplace_back(CppImpl::Interaction {
if (!priv->ready_ && priv->cpp) {
priv->cpp->interactionsBuffer_.emplace_back(CppImpl::Interaction {
uid, interactionId, interaction});
} else if (uid == priv->conversation_->uid) {
print_interaction_to_buffer(self, interactionId, interaction);
}
});
priv->cpp_ = new CppImpl();
priv->cpp = new CppImpl();
if (webkit_chat_container_is_ready(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container)))
webkit_chat_container_ready(self);
......@@ -728,7 +1014,8 @@ build_chat_view(ChatView* self)
GtkWidget *
chat_view_new (WebKitChatContainer* webkit_chat_container,
AccountInfoPointer const & accountInfo,
lrc::api::conversation::Info* conversation)
lrc::api::conversation::Info* conversation,
lrc::api::AVModel& avModel)
{
ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));
......@@ -738,6 +1025,8 @@ chat_view_new (WebKitChatContainer* webkit_chat_container,
priv->accountInfo_ = &accountInfo;
build_chat_view(self);
priv->cpp->avModel_ = &avModel;
priv->readyToRecord_ = true;
return (GtkWidget *)self;
}
......@@ -762,3 +1051,10 @@ chat_view_set_header_visible(ChatView *self, gboolean visible)
auto priv = CHAT_VIEW_GET_PRIVATE(self);
webkit_chat_set_header_visible(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container), visible);
}
void
chat_view_set_record_visible(ChatView *self, gboolean visible)
{
auto priv = CHAT_VIEW_GET_PRIVATE(self);
webkit_chat_set_record_visible(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container), visible);
}
......@@ -34,6 +34,7 @@ namespace lrc
{
namespace api
{
class AVModel;
namespace conversation
{
struct Info;
......@@ -55,9 +56,11 @@ typedef struct _ChatViewClass ChatViewClass;
GType chat_view_get_type (void) G_GNUC_CONST;
GtkWidget *chat_view_new (WebKitChatContainer* view,
AccountInfoPointer const & accountInfo,
lrc::api::conversation::Info* conversation);
lrc::api::conversation::Info* conversation,
lrc::api::AVModel& avModel);
lrc::api::conversation::Info chat_view_get_conversation(ChatView*);
void chat_view_update_temporary(ChatView*);
void chat_view_set_header_visible(ChatView*, gboolean);
void chat_view_set_record_visible(ChatView*, gboolean);
G_END_DECLS
......@@ -1298,10 +1298,11 @@ CppImpl::setCallInfo()
// init chat view
widgets->chat_view = chat_view_new(WEBKIT_CHAT_CONTAINER(widgets->webkit_chat_container),
*accountInfo, conversation);
*accountInfo, conversation, *avModel_);
gtk_container_add(GTK_CONTAINER(widgets->frame_chat), widgets->chat_view);
chat_view_set_header_visible(CHAT_VIEW(widgets->chat_view), FALSE);
chat_view_set_record_visible(CHAT_VIEW(widgets->chat_view), FALSE);
}
void
......
......@@ -83,6 +83,8 @@ struct _IncomingCallViewPrivate
QMetaObject::Connection state_change_connection;
GSettings *settings;
lrc::api::AVModel* avModel_;
};
G_DEFINE_TYPE_WITH_PRIVATE(IncomingCallView, incoming_call_view, GTK_TYPE_BOX);
......@@ -295,9 +297,11 @@ set_call_info(IncomingCallView *view) {
auto chat_view = chat_view_new(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
*priv->accountInfo_,
priv->conversation_);
priv->conversation_,
*priv->avModel_);
gtk_widget_show(chat_view);
chat_view_set_header_visible(CHAT_VIEW(chat_view), FALSE);
chat_view_set_record_visible(CHAT_VIEW(chat_view), FALSE);
gtk_container_add(GTK_CONTAINER(priv->frame_chat), chat_view);
}
......@@ -313,6 +317,7 @@ incoming_call_view_new(WebKitChatContainer* view,
priv->webkit_chat_container = GTK_WIDGET(view);
priv->conversation_ = conversation;
priv->accountInfo_ = &accountInfo;
priv->avModel_ = &avModel;
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);
......
......@@ -592,5 +592,4 @@ media_settings_view_show_preview(MediaSettingsView *self, gboolean show_preview)
priv->cpp->avModel_->setAudioMeterState(false);
priv->cpp->avModel_->stopAudioDevice();
}
}
......@@ -1489,7 +1489,7 @@ GtkWidget*
CppImpl::displayChatView(lrc::api::conversation::Info conversation, bool redraw_webview)
{
chatViewConversation_.reset(new lrc::api::conversation::Info(conversation));
auto* new_view = chat_view_new(webkitChatContainer(redraw_webview), accountInfo_, chatViewConversation_.get());
auto* new_view = chat_view_new(webkitChatContainer(redraw_webview), accountInfo_, chatViewConversation_.get(), lrc_->getAVModel());
g_signal_connect_swapped(new_view, "hide-view-clicked", G_CALLBACK(on_hide_view_clicked), self);
g_signal_connect(new_view, "add-conversation-clicked", G_CALLBACK(on_add_conversation_clicked), self);
g_signal_connect(new_view, "place-audio-call-clicked", G_CALLBACK(on_place_audio_call_clicked), self);
......
......@@ -761,6 +761,14 @@ webkit_chat_set_header_visible(WebKitChatContainer *view, bool isVisible)
g_free(function_call);
}
void
webkit_chat_set_record_visible(WebKitChatContainer *view, bool isVisible)
{
gchar* function_call = g_strdup_printf("displayRecordControls(%s)", isVisible ? "true" : "false");
webkit_chat_container_execute_js(view, function_call);
g_free(function_call);
}
void
webkit_chat_update_chatview_frame(WebKitChatContainer *view, bool accountEnabled, bool isBanned, bool isTemporary, const gchar* alias, const gchar* bestId)
{
......
......@@ -55,6 +55,7 @@ gboolean webkit_chat_container_is_ready (WebKitChatContainer *view
void webkit_chat_container_set_display_links (WebKitChatContainer *view, bool display);
void webkit_chat_container_set_invitation (WebKitChatContainer *view, bool show, const std::string& contactUri, const std::string& contactId);
void webkit_chat_set_header_visible (WebKitChatContainer *view, bool isVisible);
void webkit_chat_set_record_visible (WebKitChatContainer *view, bool isVisible);
void webkit_chat_update_chatview_frame (WebKitChatContainer *view, bool accountEnabled, bool isBanned, bool isInvited, const gchar* alias, const gchar* bestId);
G_END_DECLS
......@@ -4,101 +4,6 @@
<template class="ChatView" parent="GtkBox">
<property name="orientation">vertical</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>
<child>
<object class="GtkButton" id="button_close_chatview">
<property name="image">image_back_arrow</property>
<property name="visible">True</property>
<property name="relief">none</property>
<property name="tooltip-text" translatable="yes">Hide chat view</property>
<child internal-child="accessible">
<object class="AtkObject" id="button_close_chatview-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">Hide chat view</property>
</object>
</child>
</object>
</child>
<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="GtkButton" id="button_placecall">
<property name="visible">True</property>
<property name="image">image_place_call</property>
<property name="tooltip-text" translatable="yes">Place call</property>
<child internal-child="accessible">
<object class="AtkObject" id="button_placecall-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">Place call</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_place_audio_call">
<property name="image">image_place_audio_only_call</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip-text" translatable="yes">Place audio-only call</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_add_to_conversations">
<property name="visible">True</property>
<property name="image">image_invite</property>
<property name="tooltip-text" translatable="yes">Add to conversations</property>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_cm">
<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="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">
......
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