aboutsummaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt86
-rw-r--r--main/data/add_conversation/add_contact_dialog.ui150
-rw-r--r--main/data/add_conversation/add_groupchat_dialog.ui224
-rw-r--r--main/data/add_conversation/conference_details_fragment.ui227
-rw-r--r--main/data/add_conversation/list_row.ui61
-rw-r--r--main/data/add_conversation/select_jid_fragment.ui109
-rw-r--r--main/data/chat_input.ui23
-rw-r--r--main/data/conversation_list_titlebar.ui41
-rw-r--r--main/data/conversation_selector/chat_row_tooltip.ui23
-rw-r--r--main/data/conversation_selector/conversation_row.ui146
-rw-r--r--main/data/conversation_selector/view.ui33
-rw-r--r--main/data/conversation_summary/message_item.ui98
-rw-r--r--main/data/conversation_summary/view.ui33
-rw-r--r--main/data/conversation_titlebar.ui63
-rw-r--r--main/data/gschemas.compiledbin0 -> 316 bytes
-rw-r--r--main/data/img/double_tick.svg190
-rw-r--r--main/data/img/send.svg1
-rw-r--r--main/data/img/status_away.svg73
-rw-r--r--main/data/img/status_chat.svg85
-rw-r--r--main/data/img/status_dnd.svg73
-rw-r--r--main/data/img/status_online.svg67
-rw-r--r--main/data/img/tick.svg184
-rw-r--r--main/data/manage_accounts/account_row.ui30
-rw-r--r--main/data/manage_accounts/add_account_dialog.ui137
-rw-r--r--main/data/manage_accounts/dialog.ui308
-rw-r--r--main/data/menu_add.ui16
-rw-r--r--main/data/menu_app.ui20
-rw-r--r--main/data/menu_conversation.ui9
-rw-r--r--main/data/menu_encryption.ui33
-rw-r--r--main/data/occupant_list.ui43
-rw-r--r--main/data/occupant_list_item.ui44
-rw-r--r--main/data/settings.gschema.xml15
-rw-r--r--main/data/settings_dialog.ui51
-rw-r--r--main/data/style.css3
-rw-r--r--main/data/unified_window.ui178
-rw-r--r--main/src/main.vala2
-rw-r--r--main/src/ui/add_conversation/chat/add_contact_dialog.vala56
-rw-r--r--main/src/ui/add_conversation/chat/dialog.vala83
-rw-r--r--main/src/ui/add_conversation/chat/roster_list.vala79
-rw-r--r--main/src/ui/add_conversation/conference/add_groupchat_dialog.vala91
-rw-r--r--main/src/ui/add_conversation/conference/conference_details_fragment.vala147
-rw-r--r--main/src/ui/add_conversation/conference/conference_list.vala101
-rw-r--r--main/src/ui/add_conversation/conference/dialog.vala166
-rw-r--r--main/src/ui/add_conversation/list_row.vala39
-rw-r--r--main/src/ui/add_conversation/select_jid_fragment.vala115
-rw-r--r--main/src/ui/application.vala97
-rw-r--r--main/src/ui/avatar_generator.vala235
-rw-r--r--main/src/ui/chat_input.vala119
-rw-r--r--main/src/ui/conversation_list_titlebar.vala47
-rw-r--r--main/src/ui/conversation_selector/chat_row.vala90
-rw-r--r--main/src/ui/conversation_selector/conversation_row.vala160
-rw-r--r--main/src/ui/conversation_selector/groupchat_row.vala35
-rw-r--r--main/src/ui/conversation_selector/list.vala175
-rw-r--r--main/src/ui/conversation_selector/view.vala52
-rw-r--r--main/src/ui/conversation_summary/merged_message_item.vala159
-rw-r--r--main/src/ui/conversation_summary/merged_status_item.vala31
-rw-r--r--main/src/ui/conversation_summary/status_item.vala30
-rw-r--r--main/src/ui/conversation_summary/view.vala220
-rw-r--r--main/src/ui/conversation_titlebar.vala131
-rw-r--r--main/src/ui/manage_accounts/account_row.vala22
-rw-r--r--main/src/ui/manage_accounts/add_account_dialog.vala62
-rw-r--r--main/src/ui/manage_accounts/dialog.vala230
-rw-r--r--main/src/ui/notifications.vala57
-rw-r--r--main/src/ui/occupant_list.vala111
-rw-r--r--main/src/ui/occupant_list_row.vala25
-rw-r--r--main/src/ui/settings_dialog.vala24
-rw-r--r--main/src/ui/unified_window.vala81
-rw-r--r--main/src/ui/util.vala75
68 files changed, 6021 insertions, 3 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index ce00206e..6547f752 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -1,6 +1,7 @@
find_package(Vala REQUIRED)
find_package(PkgConfig REQUIRED)
include(${VALA_USE_FILE})
+include(GlibCompileResourcesSupport)
set(MAIN_PACKAGES
gee-0.8
@@ -8,20 +9,101 @@ set(MAIN_PACKAGES
glib-2.0
gtk+-3.0
gmodule-2.0
+ libnotify
sqlite3
)
pkg_check_modules(MAIN REQUIRED ${MAIN_PACKAGES})
+set(RESOURCE_LIST
+ img/double_tick.svg
+ img/status_away.svg
+ img/status_chat.svg
+ img/status_dnd.svg
+ img/status_online.svg
+ img/tick.svg
+
+ add_conversation/add_contact_dialog.ui
+ add_conversation/add_groupchat_dialog.ui
+ add_conversation/conference_details_fragment.ui
+ add_conversation/list_row.ui
+ add_conversation/select_jid_fragment.ui
+ chat_input.ui
+ conversation_list_titlebar.ui
+ conversation_selector/view.ui
+ conversation_selector/chat_row_tooltip.ui
+ conversation_selector/conversation_row.ui
+ conversation_summary/message_item.ui
+ conversation_summary/view.ui
+ conversation_titlebar.ui
+ manage_accounts/account_row.ui
+ manage_accounts/add_account_dialog.ui
+ manage_accounts/dialog.ui
+ menu_add.ui
+ menu_app.ui
+ menu_conversation.ui
+ menu_encryption.ui
+ occupant_list.ui
+ occupant_list_item.ui
+ style.css
+ settings_dialog.ui
+ unified_window.ui
+)
+
+compile_gresources(
+ MAIN_GRESOURCES_TARGET
+ MAIN_GRESOURCES_XML
+ TARGET ${CMAKE_CURRENT_BINARY_DIR}/resources/resources.c
+ TYPE EMBED_C
+ RESOURCES ${RESOURCE_LIST}
+ PREFIX /org/dino-im
+ SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data
+)
+
vala_precompile(MAIN_VALA_C
SOURCES
src/main.vala
+
+ src/ui/application.vala
+ src/ui/add_conversation/chat/add_contact_dialog.vala
+ src/ui/add_conversation/chat/roster_list.vala
+ src/ui/add_conversation/chat/dialog.vala
+ src/ui/add_conversation/conference/add_groupchat_dialog.vala
+ src/ui/add_conversation/conference/conference_details_fragment.vala
+ src/ui/add_conversation/conference/conference_list.vala
+ src/ui/add_conversation/conference/dialog.vala
+ src/ui/add_conversation/list_row.vala
+ src/ui/add_conversation/select_jid_fragment.vala
+ src/ui/avatar_generator.vala
+ src/ui/chat_input.vala
+ src/ui/conversation_list_titlebar.vala
+ src/ui/conversation_selector/chat_row.vala
+ src/ui/conversation_selector/conversation_row.vala
+ src/ui/conversation_selector/groupchat_row.vala
+ src/ui/conversation_selector/list.vala
+ src/ui/conversation_selector/view.vala
+ src/ui/conversation_summary/merged_message_item.vala
+ src/ui/conversation_summary/merged_status_item.vala
+ src/ui/conversation_summary/status_item.vala
+ src/ui/conversation_summary/view.vala
+ src/ui/conversation_titlebar.vala
+ src/ui/manage_accounts/account_row.vala
+ src/ui/manage_accounts/add_account_dialog.vala
+ src/ui/manage_accounts/dialog.vala
+ src/ui/notifications.vala
+ src/ui/occupant_list.vala
+ src/ui/occupant_list_row.vala
+ src/ui/settings_dialog.vala
+ src/ui/unified_window.vala
+ src/ui/util.vala
CUSTOM_VAPIS
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
${CMAKE_BINARY_DIR}/exports/qlite.vapi
${CMAKE_BINARY_DIR}/exports/dino_internal.vapi
PACKAGES
${MAIN_PACKAGES}
+GRESOURCES
+ ${MAIN_GRESOURCES_XML}
OPTIONS
--target-glib=2.38
${GLOBAL_DEBUG_FLAGS}
@@ -30,6 +112,6 @@ OPTIONS
set(CFLAGS ${VALA_CFLAGS} ${MAIN_CFLAGS})
add_definitions(${CFLAGS})
-add_executable(dino ${MAIN_VALA_C})
+add_executable(dino ${MAIN_VALA_C} ${MAIN_GRESOURCES_TARGET})
add_dependencies(dino dino-vapi)
-target_link_libraries(dino libdino) \ No newline at end of file
+target_link_libraries(dino libdino ${MAIN_LIBRARIES}) \ No newline at end of file
diff --git a/main/data/add_conversation/add_contact_dialog.ui b/main/data/add_conversation/add_contact_dialog.ui
new file mode 100644
index 00000000..58c13e7f
--- /dev/null
+++ b/main/data/add_conversation/add_contact_dialog.ui
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationChatAddContactDialog">
+ <property name="default_width">300</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="sensitive">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="has_default">True</property>
+ <property name="can_default">True</property>
+ <property name="label">Add</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="info_grid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="subscribe_checkbutton">
+ <property name="active">True</property>
+ <property name="label">Request presence updates</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/main/data/add_conversation/add_groupchat_dialog.ui b/main/data/add_conversation/add_groupchat_dialog.ui
new file mode 100644
index 00000000..c6390374
--- /dev/null
+++ b/main/data/add_conversation/add_groupchat_dialog.ui
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationConferenceAddGroupchatDialog">
+ <property name="default_width">400</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="sensitive">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="main">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="margin_right">40</property>
+ <property name="margin_left">40</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="accounts_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">combobox</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="account_label">
+ <property name="xalign">0</property>
+ <property name="can_focus">True</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Nick</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="nick_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="activates_default">True</property>
+ <property name="visibility">False</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="autojoin_checkbutton">
+ <property name="active">False</property>
+ <property name="label">Join on startup</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="alias_label">
+ <property name="label">Alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/main/data/add_conversation/conference_details_fragment.ui b/main/data/add_conversation/conference_details_fragment.ui
new file mode 100644
index 00000000..403d9a94
--- /dev/null
+++ b/main/data/add_conversation/conference_details_fragment.ui
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationConferenceConferenceDetailsFragment">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="margin_right">40</property>
+ <property name="margin_left">40</property>
+ <property name="row-spacing">7</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Account</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="accounts_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="accounts_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="accounts_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="accounts_comboboxtext">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="jid_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="jid_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Nick</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="nick_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="nick_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="nick_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="nick_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="password_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="password_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="password_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visibility">False</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/add_conversation/list_row.ui b/main/data/add_conversation/list_row.ui
new file mode 100644
index 00000000..8f011bb8
--- /dev/null
+++ b/main/data/add_conversation/list_row.ui
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationListRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="outer_grid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">3</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">30</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="via_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/add_conversation/select_jid_fragment.ui b/main/data/add_conversation/select_jid_fragment.ui
new file mode 100644
index 00000000..612f1597
--- /dev/null
+++ b/main/data/add_conversation/select_jid_fragment.ui
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiAddConversationSelectJidFragment">
+ <property name="height_request">500</property>
+ <property name="width_request">460</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="expand">True</property>
+ <property name="margin-top">20</property>
+ <property name="margin-right">80</property>
+ <property name="margin-bottom">20</property>
+ <property name="margin-left">80</property>
+ <property name="orientation">vertical</property>
+ <property name="row-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkEntry" id="entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="expand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="hscrollbar_policy">never</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolbar">
+ <property name="visible">True</property>
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <child>
+ <object class="GtkToolItem">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="edit_button">
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">document-edit-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">list-remove-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface> \ No newline at end of file
diff --git a/main/data/chat_input.ui b/main/data/chat_input.ui
new file mode 100644
index 00000000..dac75feb
--- /dev/null
+++ b/main/data/chat_input.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiChatInput" parent="GtkGrid"> <!-- TODO -->
+ <property name="hexpand">True</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <property name="margin">5</property>
+ <property name="column-spacing">5</property>
+ <child>
+ <object class="GtkFrame">
+ <child>
+ <object class="GtkTextView" id="text_input">
+ <property name="border-width">5</property>
+ <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/conversation_list_titlebar.ui b/main/data/conversation_list_titlebar.ui
new file mode 100644
index 00000000..6a5996df
--- /dev/null
+++ b/main/data/conversation_list_titlebar.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationListTitlebar" parent="GtkHeaderBar">
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="left_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="add_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="search_button">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-search-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/conversation_selector/chat_row_tooltip.ui b/main/data/conversation_selector/chat_row_tooltip.ui
new file mode 100644
index 00000000..90fbd712
--- /dev/null
+++ b/main/data/conversation_selector/chat_row_tooltip.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkBox" id="main_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="inner_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/main/data/conversation_selector/conversation_row.ui b/main/data/conversation_selector/conversation_row.ui
new file mode 100644
index 00000000..5f8498e9
--- /dev/null
+++ b/main/data/conversation_selector/conversation_row.ui
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSelectorConversationRow">
+ <property name="visible">True</property>
+ <style>
+ <class name="no_padding"/>
+ </style>
+ <child>
+ <object class="GtkRevealer" id="main_revealer">
+ <property name="transition-type">slide-down</property>
+ <property name="transition-duration">200</property>
+ <property name="reveal-child">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">7</property>
+ <property name="margin-right">14</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">40</property>
+ <property name="width_request">40</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="margin-right">7</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="time_revealer">
+ <property name="transition-type">slide-right</property>
+ <property name="transition-duration">100</property>
+ <property name="reveal-child">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="time_label">
+ <property name="hexpand">False</property>
+ <property name="xalign">1</property>
+ <attributes>
+ <attribute name="scale" value="0.7"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="message_label">
+ <property name="max_width_chars">1</property>
+ <property name="ellipsize">end</property>
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="xbutton_revealer">
+ <property name="transition-type">slide-left</property>
+ <property name="transition-duration">100</property>
+ <property name="reveal-child">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="x_button">
+ <property name="vexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_list_row_xbutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">window-close-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/conversation_selector/view.ui b/main/data/conversation_selector/view.ui
new file mode 100644
index 00000000..4bac39bc
--- /dev/null
+++ b/main/data/conversation_selector/view.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSelectorView">
+ <property name="can_focus">True</property>
+ <property name="width_request">40</property>
+ <property name="expand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchBar" id="search_bar">
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ <property name="placeholder_text" translatable="yes">Search</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled">
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/conversation_summary/message_item.ui b/main/data/conversation_summary/message_item.ui
new file mode 100644
index 00000000..f21b4969
--- /dev/null
+++ b/main/data/conversation_summary/message_item.ui
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSummaryMergedMessageItem">
+ <property name="hexpand">True</property>
+ <property name="column-spacing">7</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="expand">False</property>
+ <property name="height_request">30</property>
+ <property name="margin_top">2</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ <property name="yalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="time_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="encryption_image">
+ <property name="visible">False</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="received_image">
+ <property name="visible">False</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView" id="message_text_view">
+ <property name="editable">False</property>
+ <property name="hexpand">True</property>
+ <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </template>
+</interface> \ No newline at end of file
diff --git a/main/data/conversation_summary/view.ui b/main/data/conversation_summary/view.ui
new file mode 100644
index 00000000..74fb507e
--- /dev/null
+++ b/main/data/conversation_summary/view.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationSummaryView">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin">15</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="main">
+ <property name="expand">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">15</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="filler">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/conversation_titlebar.ui b/main/data/conversation_titlebar.ui
new file mode 100644
index 00000000..e173bdf3
--- /dev/null
+++ b/main/data/conversation_titlebar.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiConversationTitlebar" parent="GtkHeaderBar">
+ <property name="title"></property>
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="encryption_button">
+ <property name="visible">False</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="groupchat_button">
+ <property name="visible">False</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/gschemas.compiled b/main/data/gschemas.compiled
new file mode 100644
index 00000000..3a010b95
--- /dev/null
+++ b/main/data/gschemas.compiled
Binary files differ
diff --git a/main/data/img/double_tick.svg b/main/data/img/double_tick.svg
new file mode 100644
index 00000000..d65840f6
--- /dev/null
+++ b/main/data/img/double_tick.svg
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ 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"
+ sodipodi:docname="double_tick.svg"
+ inkscape:export-filename="/home/sam/source-symbolic.png"
+ inkscape:export-xdpi="270"
+ inkscape:export-ydpi="270"
+ height="16"
+ id="svg7384"
+ style="enable-background:new"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ width="16">
+ <sodipodi:namedview
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ bordercolor="#666666"
+ borderlayer="false"
+ borderopacity="1"
+ inkscape:current-layer="g8784"
+ inkscape:cx="11.598048"
+ inkscape:cy="11.93762"
+ gridtolerance="10"
+ inkscape:guide-bbox="true"
+ guidetolerance="10"
+ id="namedview88"
+ inkscape:object-nodes="true"
+ inkscape:object-paths="true"
+ objecttolerance="10"
+ pagecolor="#f7f7f7"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ showborder="true"
+ showgrid="false"
+ showguides="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-bbox="true"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-center="false"
+ inkscape:snap-global="true"
+ inkscape:snap-grids="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-others="true"
+ inkscape:snap-page="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-to-guides="true"
+ inkscape:window-height="845"
+ inkscape:window-maximized="1"
+ inkscape:window-width="1600"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:zoom="22.627416"
+ units="mm">
+ <inkscape:grid
+ color="#000000"
+ dotted="false"
+ empcolor="#0800ff"
+ empopacity="0.4627451"
+ empspacing="4"
+ enabled="true"
+ id="grid4866"
+ opacity="0.16470588"
+ originx="-104.00001px"
+ originy="-96px"
+ snapvisiblegridlinesonly="true"
+ spacingx="0.25px"
+ spacingy="0.25px"
+ type="xygrid"
+ visible="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata90">
+ <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>Paper Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title8473">Paper Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ id="linearGradient5606"
+ osb:paint="solid">
+ <stop
+ id="stop5608"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter7554"
+ color-interpolation-filters="sRGB">
+ <feBlend
+ inkscape:collect="always"
+ id="feBlend7556"
+ in2="BackgroundImage"
+ mode="darken" />
+ </filter>
+ </defs>
+ <g
+ inkscape:groupmode="layer"
+ id="layer9"
+ inkscape:label="status"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer10"
+ inkscape:label="devices"
+ style="display:inline;filter:url(#filter7554)"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer1"
+ inkscape:label="places"
+ style="display:inline"
+ transform="translate(-104.00001,-738)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer14"
+ inkscape:label="mimetypes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer15"
+ inkscape:label="emblems"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g71291"
+ inkscape:label="emotes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="categories"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g6058"
+ inkscape:label="apps"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer12"
+ inkscape:label="actions"
+ style="display:inline"
+ transform="translate(-345.00021,-121)">
+ <g
+ id="g8784"
+ inkscape:label="object-select">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 356.03145,124.03125 c -0.21888,0.0473 -0.42059,0.17053 -0.5625,0.34375 l -6.28125,7.1875 -2.25,-2.25 c -0.37633,-0.37638 -1.06119,-0.3764 -1.43755,-5e-5 -0.37635,0.37636 -0.37633,1.06122 5e-5,1.43755 l 3,3 0.78125,0.75 0.6875,-0.8125 7,-8 c 0.56742,-0.61773 -0.11583,-1.8248 -0.9375,-1.65625 z"
+ id="path8741"
+ sodipodi:nodetypes="ccccscccccc"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" />
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2.13333344;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccc"
+ id="path4500"
+ d="m 359.74533,124.03232 c -0.23347,0.0504 -0.44863,0.1819 -0.6,0.36667 l -6.7,7.66666 -0.34583,-0.34583 -1.42592,1.61866 1.03842,1.06051 0.83333,0.8 0.73334,-0.86667 7.46666,-8.53333 c 0.60525,-0.65892 -0.12355,-1.94646 -1,-1.76667 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/main/data/img/send.svg b/main/data/img/send.svg
new file mode 100644
index 00000000..8627d4a7
--- /dev/null
+++ b/main/data/img/send.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#6f778c" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" /></svg>
diff --git a/main/data/img/status_away.svg b/main/data/img/status_away.svg
new file mode 100644
index 00000000..d976d095
--- /dev/null
+++ b/main/data/img/status_away.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<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="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ sodipodi:docname="status_away.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="873"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <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 />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#ffa726;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.85716,698.38898 v 35.6875 h 35.6875"
+ id="path4157"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ </g>
+</svg>
diff --git a/main/data/img/status_chat.svg b/main/data/img/status_chat.svg
new file mode 100644
index 00000000..5b427cb6
--- /dev/null
+++ b/main/data/img/status_chat.svg
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<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="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_chat.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#81c784;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:#81c784;fill-rule:evenodd;stroke:#ffffff;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ d="m 77.857162,738.07648 c 13.92857,35.35715 55.714288,35 69.999998,0"
+ id="path4199"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4203"
+ cx="90.357162"
+ cy="710.57648"
+ r="5" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4203-3"
+ cx="135.35716"
+ cy="710.57648"
+ r="5" />
+ </g>
+</svg>
diff --git a/main/data/img/status_dnd.svg b/main/data/img/status_dnd.svg
new file mode 100644
index 00000000..e7e17e78
--- /dev/null
+++ b/main/data/img/status_dnd.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<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="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_dnd.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <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 />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#e57373;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 82.857162,728.07648 59.999998,0"
+ id="path4178"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ </g>
+</svg>
diff --git a/main/data/img/status_online.svg b/main/data/img/status_online.svg
new file mode 100644
index 00000000..13cc6592
--- /dev/null
+++ b/main/data/img/status_online.svg
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<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="28.222221mm"
+ height="28.222221mm"
+ viewBox="0 0 99.999997 99.999997"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status_online.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="-2.3899949"
+ inkscape:cy="49.421164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1600"
+ inkscape:window-height="845"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <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>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-62.857162,-678.07648)">
+ <circle
+ style="opacity:1;fill:#81c784;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path4136"
+ cx="112.85716"
+ cy="728.07648"
+ r="50" />
+ </g>
+</svg>
diff --git a/main/data/img/tick.svg b/main/data/img/tick.svg
new file mode 100644
index 00000000..4a08848c
--- /dev/null
+++ b/main/data/img/tick.svg
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ 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"
+ sodipodi:docname="tick.svg"
+ inkscape:export-filename="/home/sam/source-symbolic.png"
+ inkscape:export-xdpi="270"
+ inkscape:export-ydpi="270"
+ height="16"
+ id="svg7384"
+ style="enable-background:new"
+ version="1.1"
+ inkscape:version="0.92.0 r"
+ width="16">
+ <sodipodi:namedview
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ bordercolor="#666666"
+ borderlayer="false"
+ borderopacity="1"
+ inkscape:current-layer="g8784"
+ inkscape:cx="11.077638"
+ inkscape:cy="13.807036"
+ gridtolerance="10"
+ inkscape:guide-bbox="true"
+ guidetolerance="10"
+ id="namedview88"
+ inkscape:object-nodes="true"
+ inkscape:object-paths="true"
+ objecttolerance="10"
+ pagecolor="#f7f7f7"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ showborder="true"
+ showgrid="false"
+ showguides="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-bbox="true"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-center="false"
+ inkscape:snap-global="true"
+ inkscape:snap-grids="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-others="true"
+ inkscape:snap-page="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-to-guides="true"
+ inkscape:window-height="838"
+ inkscape:window-maximized="0"
+ inkscape:window-width="1290"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:zoom="11.313708"
+ units="mm">
+ <inkscape:grid
+ color="#000000"
+ dotted="false"
+ empcolor="#0800ff"
+ empopacity="0.4627451"
+ empspacing="4"
+ enabled="true"
+ id="grid4866"
+ opacity="0.16470588"
+ originx="-104.00001px"
+ originy="-96px"
+ snapvisiblegridlinesonly="true"
+ spacingx="0.25px"
+ spacingy="0.25px"
+ type="xygrid"
+ visible="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata90">
+ <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>Paper Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title8473">Paper Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ id="linearGradient5606"
+ osb:paint="solid">
+ <stop
+ id="stop5608"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter7554"
+ color-interpolation-filters="sRGB">
+ <feBlend
+ inkscape:collect="always"
+ id="feBlend7556"
+ in2="BackgroundImage"
+ mode="darken" />
+ </filter>
+ </defs>
+ <g
+ inkscape:groupmode="layer"
+ id="layer9"
+ inkscape:label="status"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer10"
+ inkscape:label="devices"
+ style="display:inline;filter:url(#filter7554)"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer1"
+ inkscape:label="places"
+ style="display:inline"
+ transform="translate(-104.00001,-738)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer14"
+ inkscape:label="mimetypes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer15"
+ inkscape:label="emblems"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g71291"
+ inkscape:label="emotes"
+ style="display:inline"
+ transform="translate(-345.00021,-121)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="categories"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="g6058"
+ inkscape:label="apps"
+ style="display:inline"
+ transform="translate(-104.00001,-588)" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer12"
+ inkscape:label="actions"
+ style="display:inline"
+ transform="translate(-345.00021,-121)">
+ <g
+ id="g8784"
+ inkscape:label="object-select">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 356.03145,124.03125 c -0.21888,0.0473 -0.42059,0.17053 -0.5625,0.34375 l -6.28125,7.1875 -2.25,-2.25 c -0.37633,-0.37638 -1.06119,-0.3764 -1.43755,-5e-5 -0.37635,0.37636 -0.37633,1.06122 5e-5,1.43755 l 3,3 0.78125,0.75 0.6875,-0.8125 7,-8 c 0.56742,-0.61773 -0.11583,-1.8248 -0.9375,-1.65625 z"
+ id="path8741"
+ sodipodi:nodetypes="ccccscccccc"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#555555;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" />
+ </g>
+ </g>
+</svg>
diff --git a/main/data/manage_accounts/account_row.ui b/main/data/manage_accounts/account_row.ui
new file mode 100644
index 00000000..ab700daa
--- /dev/null
+++ b/main/data/manage_accounts/account_row.ui
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiManageAccountsAccountRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">6</property>
+ <property name="column-spacing">6</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">40</property>
+ <property name="width_request">40</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="halign">0.5</property>
+ <property name="xalign">0</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/manage_accounts/add_account_dialog.ui b/main/data/manage_accounts/add_account_dialog.ui
new file mode 100644
index 00000000..dd5264f1
--- /dev/null
+++ b/main/data/manage_accounts/add_account_dialog.ui
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiManageAccountsAddAccountDialog">
+ <property name="default_width">300</property>
+ <property name="modal">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label">Cancel</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="can_default">True</property>
+ <property name="label">Save</property>
+ <property name="sensitive">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="info_grid">
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <property name="column-spacing">10</property>
+ <property name="row-spacing">7</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">JID</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="jid_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="input_purpose">password</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="visibility">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Local alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="ok" default="true">ok_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/main/data/manage_accounts/dialog.ui b/main/data/manage_accounts/dialog.ui
new file mode 100644
index 00000000..e431bfff
--- /dev/null
+++ b/main/data/manage_accounts/dialog.ui
@@ -0,0 +1,308 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiManageAccountsDialog">
+ <property name="width-request">700</property>
+ <property name="height-request">400</property>
+ <property name="visible">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="title">Accounts</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="main_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">True</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="expand">True</property>
+ <property name="orientation">horizontal</property>
+ <property name="margin">15</property>
+ <property name="spacing">20</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="width-request">250</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="shadow-type">in</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="account_list">
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolbar">
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <property name="icon-size">menu</property>
+ <property name="toolbar-style">icons</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolButton" id="add_button">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="remove_button">
+ <property name="icon-name">list-remove-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="settings_list">
+ <property name="expand">True</property>
+ <property name="column-spacing">10</property>
+ <property name="row-spacing">5</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="image_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">50</property>
+ <property name="width_request">50</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="jid_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="active_switch">
+ <property name="visible">True</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Password</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="password_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="password_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="password_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="hexpand">True</property>
+ <property name="input_purpose">password</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="visibility">False</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Local alias</property>
+ <property name="xalign">1</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="alias_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="alias_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="alias_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="alias_entry">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">accounts_exist</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">4</property>
+ <property name="pixel-size">72</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">No accounts configured</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="no_accounts_add">
+ <property name="label">Add an account</property>
+ <property name="halign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="text-button"/>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">no_accounts</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/menu_add.ui b/main/data/menu_add.ui
new file mode 100644
index 00000000..3b2c4c4a
--- /dev/null
+++ b/main/data/menu_add.ui
@@ -0,0 +1,16 @@
+<interface>
+ <menu id="menu_add">
+ <section>
+ <item>
+ <attribute name="action">app.add_chat</attribute>
+ <attribute name="label" translatable="yes">Start Chat</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="action">app.add_conference</attribute>
+ <attribute name="label" translatable="yes">Join Conference</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/main/data/menu_app.ui b/main/data/menu_app.ui
new file mode 100644
index 00000000..d3fa4cb7
--- /dev/null
+++ b/main/data/menu_app.ui
@@ -0,0 +1,20 @@
+<interface>
+ <menu id="menu_app">
+ <section>
+ <item>
+ <attribute name="action">app.accounts</attribute>
+ <attribute name="label">Accounts</attribute>
+ </item>
+ <item>
+ <attribute name="action">app.settings</attribute>
+ <attribute name="label">Settings</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="action">app.quit</attribute>
+ <attribute name="label">Quit</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/main/data/menu_conversation.ui b/main/data/menu_conversation.ui
new file mode 100644
index 00000000..9fe2a2b7
--- /dev/null
+++ b/main/data/menu_conversation.ui
@@ -0,0 +1,9 @@
+<interface>
+ <menu id="menu_conversation">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Contact Details</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/main/data/menu_encryption.ui b/main/data/menu_encryption.ui
new file mode 100644
index 00000000..216bdd92
--- /dev/null
+++ b/main/data/menu_encryption.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkPopoverMenu" id="menu_encryption">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="encryption_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="margin">10</property>
+ <child>
+ <object class="GtkRadioButton" id="button_unencrypted">
+ <property name="label" translatable="yes">Unencrypted</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="submenu">main</property>
+ </packing>
+ </child>
+ </object>
+</interface> \ No newline at end of file
diff --git a/main/data/occupant_list.ui b/main/data/occupant_list.ui
new file mode 100644
index 00000000..deb4716e
--- /dev/null
+++ b/main/data/occupant_list.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiOccupantList">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolbar">
+ <property name="icon_size">1</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkToolItem">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="margin">5</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="max_content_height">500</property>
+ <property name="propagate_natural_height">True</property>
+ <property name="margin">5</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list_box">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/occupant_list_item.ui b/main/data/occupant_list_item.ui
new file mode 100644
index 00000000..aabe8a05
--- /dev/null
+++ b/main/data/occupant_list_item.ui
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiOccupantListRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">horizontal</property>
+ <property name="margin">3</property>
+ <property name="column-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="height_request">30</property>
+ <property name="width_request">30</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="expand">True</property>
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="name"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/settings.gschema.xml b/main/data/settings.gschema.xml
new file mode 100644
index 00000000..f3d342cf
--- /dev/null
+++ b/main/data/settings.gschema.xml
@@ -0,0 +1,15 @@
+<schemalist>
+ <schema id="org.dino-im" path="/org/dino-im/" gettext-domain="dino">
+
+ <key name="send-read" type="b">
+ <default>true</default>
+ <summary>Whether to confirm that a message was received per default</summary>
+ </key>
+
+ <key name="convert-utf8-smileys" type="b">
+ <default>true</default>
+ <summary>Whether to convert common ascii smileys into unicode</summary>
+ </key>
+
+ </schema>
+</schemalist> \ No newline at end of file
diff --git a/main/data/settings_dialog.ui b/main/data/settings_dialog.ui
new file mode 100644
index 00000000..3b939216
--- /dev/null
+++ b/main/data/settings_dialog.ui
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiSettingsDialog">
+ <property name="modal">True</property>
+ <property name="visible">True</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <property name="title">Preferences</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="margin">10</property>
+ <property name="row-spacing">10</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkCheckButton" id="marker_checkbutton">
+ <property name="label">Send typing notifications and message marker</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="emoji_checkbutton">
+ <property name="label">Convert smileys to emojis</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface> \ No newline at end of file
diff --git a/main/data/style.css b/main/data/style.css
new file mode 100644
index 00000000..d143ffd3
--- /dev/null
+++ b/main/data/style.css
@@ -0,0 +1,3 @@
+scrolledwindow {
+ background-color: white;
+} \ No newline at end of file
diff --git a/main/data/unified_window.ui b/main/data/unified_window.ui
new file mode 100644
index 00000000..289c00cf
--- /dev/null
+++ b/main/data/unified_window.ui
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="DinoUiWindow">
+ <property name="default-width">1200</property>
+ <property name="default-height">700</property>
+ <child type="titlebar">
+ <object class="GtkPaned">
+ <property name="position" bind-source="main_paned" bind-property="position" bind-flags="bidirectional|sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="header_bar"/>
+ </style>
+ <child>
+ <object class="GtkHeaderBar" id="left_toolbar">
+ <property name="hexpand">False</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="left_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="add_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHeaderBar" id="right_toolbar">
+ <property name="title"></property>
+ <property name="hexpand">True</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="right_toolbar"/>
+ </style>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="encryption_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="groupchat_button">
+ <property name="visible">True</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-users-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPaned" id="main_paned">
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="DinoUiRosterWrapper" id="roster_wrapper">
+ <style>
+ <class name="roster_wrapper"/>
+ </style>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="shadow-type">GTK_SHADOW_NONE</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="some_frame"/>
+ </style>
+ <child>
+ <object class="GtkGrid">
+ <property name="orientation">vertical</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_grid"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow" id="conversation_frame_scrolled">
+ <property name="visible">True</property>
+ <style>
+ <class name="scrolled_window"/>
+ </style>
+ <child>
+ <object class="DinoUiConversationFrame" id="conversation_frame">
+ <property name="shadow-type">GTK_SHADOW_NONE</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="conversation_frame"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator" />
+ </child>
+ <child>
+ <object class="DinoUiChatInput" id="chat_input">
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/src/main.vala b/main/src/main.vala
index 21e45bf4..d0e47213 100644
--- a/main/src/main.vala
+++ b/main/src/main.vala
@@ -5,7 +5,7 @@ namespace Dino {
void main(string[] args) {
Gtk.init(ref args);
- Application app = new Application();
+ Dino.Ui.Application app = new Dino.Ui.Application();
Plugins.Loader loader = new Plugins.Loader();
foreach(string plugin in new string[]{}) {
try {
diff --git a/main/src/ui/add_conversation/chat/add_contact_dialog.vala b/main/src/ui/add_conversation/chat/add_contact_dialog.vala
new file mode 100644
index 00000000..df8fbeb9
--- /dev/null
+++ b/main/src/ui/add_conversation/chat/add_contact_dialog.vala
@@ -0,0 +1,56 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Chat {
+
+[GtkTemplate (ui = "/org/dino-im/add_conversation/add_contact_dialog.ui")]
+protected class AddContactDialog : Gtk.Dialog {
+
+ [GtkChild] private ComboBoxText accounts_comboboxtext;
+ [GtkChild] private Button ok_button;
+ [GtkChild] private Button cancel_button;
+ [GtkChild] private Entry jid_entry;
+ [GtkChild] private Entry alias_entry;
+ [GtkChild] private CheckButton subscribe_checkbutton;
+
+ private StreamInteractor stream_interactor;
+
+ public AddContactDialog(StreamInteractor stream_interactor) {
+ Object(use_header_bar : 1);
+ this.stream_interactor = stream_interactor;
+
+ foreach (Account account in stream_interactor.get_accounts()) {
+ accounts_comboboxtext.append_text(account.bare_jid.to_string());
+ }
+ accounts_comboboxtext.set_active(0);
+
+ cancel_button.clicked.connect(() => { close(); });
+ ok_button.clicked.connect(on_ok_button_clicked);
+ jid_entry.changed.connect(on_jid_entry_changed);
+ }
+
+ private void on_ok_button_clicked() {
+ string? alias = alias_entry.text == "" ? null : alias_entry.text;
+ Account? account = null;
+ Jid jid = new Jid(jid_entry.text);
+ foreach (Account account2 in stream_interactor.get_accounts()) {
+ print(account2.bare_jid.to_string() + "\n");
+ if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) {
+ account = account2;
+ }
+ }
+ RosterManager.get_instance(stream_interactor).add_jid(account, jid, alias);
+ if (subscribe_checkbutton.active) {
+ PresenceManager.get_instance(stream_interactor).request_subscription(account, jid);
+ }
+ close();
+ }
+
+ private void on_jid_entry_changed() {
+ Jid parsed_jid = Jid.parse(jid_entry.text);
+ ok_button.set_sensitive(parsed_jid != null && parsed_jid.resourcepart == null);
+ }
+}
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/chat/dialog.vala b/main/src/ui/add_conversation/chat/dialog.vala
new file mode 100644
index 00000000..cad2b367
--- /dev/null
+++ b/main/src/ui/add_conversation/chat/dialog.vala
@@ -0,0 +1,83 @@
+using Gee;
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Chat {
+
+public class Dialog : Gtk.Dialog {
+
+ public signal void conversation_opened(Conversation conversation);
+
+ private Button ok_button;
+
+ private RosterList roster_list;
+ private SelectJidFragment select_jid_fragment;
+ private StreamInteractor stream_interactor;
+
+ public Dialog(StreamInteractor stream_interactor) {
+ Object(use_header_bar : 1);
+ this.title = "Start Chat";
+ this.modal = true;
+ this.stream_interactor = stream_interactor;
+
+ setup_headerbar();
+ setup_view();
+ }
+
+ private void setup_headerbar() {
+ HeaderBar header_bar = get_header_bar() as HeaderBar;
+ header_bar.show_close_button = false;
+
+ Button cancel_button = new Button();
+ cancel_button.set_label("Cancel");
+ cancel_button.visible = true;
+ header_bar.pack_start(cancel_button);
+
+ ok_button = new Button();
+ ok_button.get_style_context().add_class("suggested-action");
+ ok_button.label = "Start";
+ ok_button.sensitive = false;
+ ok_button.visible = true;
+ header_bar.pack_end(ok_button);
+
+ cancel_button.clicked.connect(() => { close(); });
+ ok_button.clicked.connect(on_ok_button_clicked);
+ }
+
+ private void setup_view() {
+ roster_list = new RosterList(stream_interactor);
+ roster_list.row_activated.connect(() => { ok_button.clicked(); });
+ select_jid_fragment = new SelectJidFragment(stream_interactor, roster_list);
+ select_jid_fragment.add_jid.connect((row) => {
+ AddContactDialog add_contact_dialog = new AddContactDialog(stream_interactor);
+ add_contact_dialog.set_transient_for(this);
+ add_contact_dialog.show();
+ });
+ select_jid_fragment.edit_jid.connect(() => {
+
+ });
+ select_jid_fragment.remove_jid.connect((row) => {
+ ListRow list_row = roster_list.get_selected_row() as ListRow;
+ RosterManager.get_instance(stream_interactor).remove_jid(list_row.account, list_row.jid);
+ });
+ select_jid_fragment.notify["done"].connect(() => {
+ ok_button.sensitive = select_jid_fragment.done;
+ });
+ get_content_area().add(select_jid_fragment);
+ }
+
+ protected void on_ok_button_clicked() {
+ ListRow? selected_row = roster_list.get_selected_row() as ListRow;
+ if (selected_row != null) {
+ // TODO move in list to front immediately
+ ConversationManager.get_instance(stream_interactor).ensure_start_conversation(selected_row.jid, selected_row.account);
+ Conversation conversation = ConversationManager.get_instance(stream_interactor).get_conversation(selected_row.jid, selected_row.account);
+ conversation_opened(conversation);
+ }
+ close();
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/chat/roster_list.vala b/main/src/ui/add_conversation/chat/roster_list.vala
new file mode 100644
index 00000000..92388597
--- /dev/null
+++ b/main/src/ui/add_conversation/chat/roster_list.vala
@@ -0,0 +1,79 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui.AddConversation.Chat {
+
+protected class RosterList : FilterableList {
+
+ public signal void conversation_selected(Conversation? conversation);
+ private StreamInteractor stream_interactor;
+
+ private HashMap<Jid, ListRow> rows = new HashMap<Jid, ListRow>(Jid.hash_func, Jid.equals_func);
+
+ public RosterList(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ set_filter_func(filter);
+ set_header_func(header);
+ set_sort_func(sort);
+
+ RosterManager.get_instance(stream_interactor).removed_roster_item.connect( (account, jid, roster_item) => {
+ Idle.add(() => { on_removed_roster_item(account, jid, roster_item); return false;});});
+ RosterManager.get_instance(stream_interactor).updated_roster_item.connect( (account, jid, roster_item) => {
+ Idle.add(() => { on_updated_roster_item(account, jid, roster_item); return false;});});
+
+ foreach (Account account in stream_interactor.get_accounts()) {
+ foreach (Roster.Item roster_item in RosterManager.get_instance(stream_interactor).get_roster(account)) {
+ on_updated_roster_item(account, new Jid(roster_item.jid), roster_item);
+ }
+ }
+ }
+
+ private void on_removed_roster_item(Account account, Jid jid, Roster.Item roster_item) {
+ if (rows.has_key(jid)) {
+ remove(rows[jid]);
+ rows.unset(jid);
+ }
+ }
+
+ private void on_updated_roster_item(Account account, Jid jid, Roster.Item roster_item) {
+ on_removed_roster_item(account, jid, roster_item);
+ ListRow row = new ListRow.from_jid(stream_interactor, new Jid(roster_item.jid), account);
+ rows[jid] = row;
+ add(row);
+ invalidate_sort();
+ invalidate_filter();
+ }
+
+ private void header(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+ private bool filter(ListBoxRow r) {
+ if (r.get_type().is_a(typeof(ListRow))) {
+ ListRow row = r as ListRow;
+ if (filter_values != null) {
+ foreach (string filter in filter_values) {
+ if (!(row.name_label.label.down().contains(filter.down()) ||
+ row.jid.to_string().down().contains(filter.down()))) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ public override int sort(ListBoxRow row1, ListBoxRow row2) {
+ ListRow c1 = (row1 as ListRow);
+ ListRow c2 = (row2 as ListRow);
+ return c1.name_label.label.collate(c2.name_label.label);
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/conference/add_groupchat_dialog.vala b/main/src/ui/add_conversation/conference/add_groupchat_dialog.vala
new file mode 100644
index 00000000..8cc5ac72
--- /dev/null
+++ b/main/src/ui/add_conversation/conference/add_groupchat_dialog.vala
@@ -0,0 +1,91 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Conference {
+
+[GtkTemplate (ui = "/org/dino-im/add_conversation/add_groupchat_dialog.ui")]
+protected class AddGroupchatDialog : Gtk.Dialog {
+
+ [GtkChild] private Stack accounts_stack;
+ [GtkChild] private ComboBoxText accounts_comboboxtext;
+ [GtkChild] private Label account_label;
+ [GtkChild] private Button ok_button;
+ [GtkChild] private Button cancel_button;
+ [GtkChild] private Entry jid_entry;
+ [GtkChild] private Entry alias_entry;
+ [GtkChild] private Entry nick_entry;
+ [GtkChild] private CheckButton autojoin_checkbutton;
+
+ private StreamInteractor stream_interactor;
+ private Xmpp.Xep.Bookmarks.Conference? edit_confrence = null;
+ private bool alias_entry_changed = false;
+
+ public AddGroupchatDialog(StreamInteractor stream_interactor) {
+ Object(use_header_bar : 1);
+ this.stream_interactor = stream_interactor;
+ ok_button.label = "Add";
+ ok_button.get_style_context().add_class("suggested-action"); // TODO why doesn't it work in XML
+ accounts_stack.set_visible_child_name("combobox");
+ foreach (Account account in stream_interactor.get_accounts()) {
+ accounts_comboboxtext.append_text(account.bare_jid.to_string());
+ }
+ accounts_comboboxtext.set_active(0);
+
+ cancel_button.clicked.connect(() => { close(); });
+ ok_button.clicked.connect(on_ok_button_clicked);
+ jid_entry.key_release_event.connect(on_jid_key_release);
+ nick_entry.key_release_event.connect(check_ok);
+ }
+
+ public AddGroupchatDialog.for_conference(StreamInteractor stream_interactor, Account account, Xmpp.Xep.Bookmarks.Conference conference) {
+ this(stream_interactor);
+ edit_confrence = conference;
+ ok_button.label = "Save";
+ ok_button.sensitive = true;
+ accounts_stack.set_visible_child_name("label");
+ account_label.label = account.bare_jid.to_string();
+ jid_entry.text = conference.jid;
+ nick_entry.text = conference.nick;
+ autojoin_checkbutton.active = conference.autojoin;
+ alias_entry.text = conference.name;
+ }
+
+ private bool on_jid_key_release() {
+ check_ok();
+ if (!alias_entry_changed) {
+ Jid? parsed_jid = Jid.parse(jid_entry.text);
+ alias_entry.text = parsed_jid != null && parsed_jid.localpart != null ? parsed_jid.localpart : jid_entry.text;
+ }
+ return false;
+ }
+
+ private bool check_ok() {
+ Jid? parsed_jid = Jid.parse(jid_entry.text);
+ ok_button.sensitive = parsed_jid != null && parsed_jid.localpart != null && parsed_jid.resourcepart == null &&
+ nick_entry.text != "" && alias_entry.text != null;
+ return false;
+ }
+
+ private void on_ok_button_clicked() {
+ Account? account = null;
+ foreach (Account account2 in stream_interactor.get_accounts()) {
+ if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) {
+ account = account2;
+ }
+ }
+ Xmpp.Xep.Bookmarks.Conference conference = new Xmpp.Xep.Bookmarks.Conference(jid_entry.text);
+ conference.nick = nick_entry.text;
+ conference.name = alias_entry.text;
+ conference.autojoin = autojoin_checkbutton.active;
+ if (edit_confrence == null) {
+ MucManager.get_instance(stream_interactor).add_bookmark(account, conference);
+ } else {
+ MucManager.get_instance(stream_interactor).replace_bookmark(account, edit_confrence, conference);
+ }
+ close();
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/conference/conference_details_fragment.vala b/main/src/ui/add_conversation/conference/conference_details_fragment.vala
new file mode 100644
index 00000000..d42c79bd
--- /dev/null
+++ b/main/src/ui/add_conversation/conference/conference_details_fragment.vala
@@ -0,0 +1,147 @@
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Conference {
+
+[GtkTemplate (ui = "/org/dino-im/add_conversation/conference_details_fragment.ui")]
+protected class ConferenceDetailsFragment : Box {
+
+ public bool done {
+ get {
+ Jid? parsed_jid = Jid.parse(jid);
+ return parsed_jid != null && parsed_jid.localpart != null &&
+ parsed_jid.resourcepart == null && nick != "";
+ }
+ private set {}
+ }
+
+ public Account account {
+ owned get {
+ foreach (Account account in stream_interactor.get_accounts()) {
+ if (accounts_comboboxtext.get_active_text() == account.bare_jid.to_string()) {
+ return account;
+ }
+ }
+ return null;
+ }
+ set {
+ accounts_label.label = value.bare_jid.to_string();
+ accounts_comboboxtext.set_active_id(value.bare_jid.to_string());
+ }
+ }
+ public string jid {
+ get { return jid_entry.text; }
+ set {
+ jid_label.label = value;
+ jid_entry.text = value;
+ }
+ }
+ public string nick {
+ get { return nick_entry.text; }
+ set {
+ nick_label.label = value;
+ nick_entry.text = value;
+ }
+ }
+ public string password {
+ get { return password_entry.text == "" ? null : password_entry.text; }
+ set {
+ password_label.label = value;
+ password_entry.text = value;
+ }
+ }
+
+ [GtkChild] private Stack accounts_stack;
+ [GtkChild] private Button accounts_button;
+ [GtkChild] private Label accounts_label;
+ [GtkChild] private ComboBoxText accounts_comboboxtext;
+
+ [GtkChild] private Stack jid_stack;
+ [GtkChild] private Button jid_button;
+ [GtkChild] private Label jid_label;
+ [GtkChild] private Entry jid_entry;
+
+ [GtkChild] private Stack nick_stack;
+ [GtkChild] private Button nick_button;
+ [GtkChild] private Label nick_label;
+ [GtkChild] private Entry nick_entry;
+
+ [GtkChild] private Stack password_stack;
+ [GtkChild] private Button password_button;
+ [GtkChild] private Label password_label;
+ [GtkChild] private Entry password_entry;
+
+ private StreamInteractor stream_interactor;
+
+ public ConferenceDetailsFragment(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ accounts_stack.set_visible_child_name("label");
+ jid_stack.set_visible_child_name("label");
+ nick_stack.set_visible_child_name("label");
+ password_stack.set_visible_child_name("label");
+
+ accounts_button.clicked.connect(() => { set_active_stack(accounts_stack); });
+ jid_button.clicked.connect(() => { set_active_stack(jid_stack); });
+ nick_button.clicked.connect(() => { set_active_stack(nick_stack); });
+ password_button.clicked.connect(() => { set_active_stack(password_stack); });
+
+ accounts_comboboxtext.changed.connect(() => {accounts_label.label = accounts_comboboxtext.get_active_text(); });
+ jid_entry.key_release_event.connect(on_jid_key_release_event);
+ nick_entry.key_release_event.connect(on_nick_key_release_event);
+ password_entry.key_release_event.connect(on_password_key_release_event);
+
+ jid_entry.key_release_event.connect(() => { done = true; return false; }); // just for notifying
+ nick_entry.key_release_event.connect(() => { done = true; return false; });
+
+ foreach (Account account in stream_interactor.get_accounts()) {
+ accounts_comboboxtext.append_text(account.bare_jid.to_string());
+ }
+ accounts_comboboxtext.set_active(0);
+ }
+
+ public void set_editable() {
+ accounts_stack.set_visible_child_name("entry");
+ nick_stack.set_visible_child_name("entry");
+ password_stack.set_visible_child_name("entry");
+ }
+
+ public void clear() {
+ jid = "";
+ nick = "";
+ password = "";
+ }
+
+ private bool on_jid_key_release_event(EventKey event) {
+ jid_label.label = jid_entry.text;
+ if (event.keyval == Key.Return) jid_stack.set_visible_child_name("label");
+ return false;
+ }
+
+ private bool on_nick_key_release_event(EventKey event) {
+ nick_label.label = nick_entry.text;
+ if (event.keyval == Key.Return) nick_stack.set_visible_child_name("label");
+ return false;
+ }
+
+ private bool on_password_key_release_event(EventKey event) {
+ string filler = "";
+ for (int i = 0; i < password_entry.text.length; i++) filler += password_entry.get_invisible_char().to_string();
+ password_label.label = filler;
+ if (event.keyval == Key.Return) password_stack.set_visible_child_name("label");
+ return false;
+ }
+
+ private void set_active_stack(Stack stack) {
+ stack.set_visible_child_name("entry");
+ if (stack != accounts_stack) accounts_stack.set_visible_child_name("label");
+ if (stack != jid_stack) jid_stack.set_visible_child_name("label");
+ if (stack != nick_stack) nick_stack.set_visible_child_name("label");
+ if (stack != password_stack) password_stack.set_visible_child_name("label");
+ }
+
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/conference/conference_list.vala b/main/src/ui/add_conversation/conference/conference_list.vala
new file mode 100644
index 00000000..7743ced5
--- /dev/null
+++ b/main/src/ui/add_conversation/conference/conference_list.vala
@@ -0,0 +1,101 @@
+using Gee;
+using Gtk;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Conference {
+
+protected class ConferenceList : FilterableList {
+
+ public signal void conversation_selected(Conversation? conversation);
+
+ private StreamInteractor stream_interactor;
+ private HashMap<Account, ArrayList<Xep.Bookmarks.Conference>> lists = new HashMap<Account, ArrayList<Xep.Bookmarks.Conference>>();
+
+ public ConferenceList(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ set_filter_func(filter);
+ set_header_func(header);
+ set_sort_func(sort);
+
+ MucManager.get_instance(stream_interactor).bookmarks_updated.connect((account, conferences) => {
+ Idle.add(() => {
+ lists[account] = conferences;
+ refresh_conferences();
+ return false;
+ });
+ });
+
+ foreach (Account account in stream_interactor.get_accounts()) {
+ MucManager.get_instance(stream_interactor).get_bookmarks(account, on_conference_bookmarks_received, Tuple.create(this, account));
+ }
+ }
+
+ public void refresh_conferences() {
+ @foreach((widget) => { remove(widget); });
+ foreach (Account account in lists.keys) {
+ foreach (Xep.Bookmarks.Conference conference in lists[account]) {
+ add(new ConferenceListRow(stream_interactor, conference, account));
+ }
+ }
+ }
+
+ private static void on_conference_bookmarks_received(Core.XmppStream stream, ArrayList<Xep.Bookmarks.Conference> conferences, Object? o) {
+ Tuple<ConferenceList, Account> tuple = o as Tuple<ConferenceList, Account>;
+ ConferenceList list = tuple.a;
+ Account account = tuple.b;
+ list.lists[account] = conferences;
+ Idle.add(() => { list.refresh_conferences(); return false; });
+ }
+
+ private void header(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+ private bool filter(ListBoxRow r) {
+ if (r.get_type().is_a(typeof(ListRow))) {
+ ListRow row = r as ListRow;
+ if (filter_values != null) {
+ foreach (string filter in filter_values) {
+ if (!(row.name_label.label.down().contains(filter.down()) ||
+ row.jid.to_string().down().contains(filter.down()))) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ public override int sort(ListBoxRow row1, ListBoxRow row2) {
+ ListRow c1 = (row1 as ListRow);
+ ListRow c2 = (row2 as ListRow);
+ return c1.name_label.label.collate(c2.name_label.label);
+ }
+}
+
+internal class ConferenceListRow : ListRow {
+
+ public Xep.Bookmarks.Conference bookmark;
+
+ public ConferenceListRow(StreamInteractor stream_interactor, Xep.Bookmarks.Conference bookmark, Account account) {
+ this.jid = new Jid(bookmark.jid);
+ this.account = account;
+ this.bookmark = bookmark;
+
+ if (bookmark.name != "" && bookmark.name != bookmark.jid) {
+ name_label.label = bookmark.name;
+ via_label.label = bookmark.jid;
+ } else {
+ name_label.label = bookmark.jid;
+ via_label.visible = false;
+ }
+ image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_stateless(true).draw_jid(stream_interactor, jid, account));
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/conference/dialog.vala b/main/src/ui/add_conversation/conference/dialog.vala
new file mode 100644
index 00000000..ff548699
--- /dev/null
+++ b/main/src/ui/add_conversation/conference/dialog.vala
@@ -0,0 +1,166 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation.Conference {
+
+public class Dialog : Gtk.Dialog {
+
+ public signal void conversation_opened(Conversation conversation);
+
+ private Stack stack = new Stack();
+ private Button cancel_button;
+ private Button ok_button;
+ private Label cancel_label = new Label("Cancel") {visible=true};
+ private Image cancel_image = new Image.from_icon_name("go-previous-symbolic", IconSize.MENU) {visible=true};
+
+ private SelectJidFragment select_fragment;
+ private ConferenceDetailsFragment details_fragment;
+ private ConferenceList conference_list;
+
+ private StreamInteractor stream_interactor;
+
+ public Dialog(StreamInteractor stream_interactor) {
+ Object(use_header_bar : 1);
+ this.title = "Join Conference";
+ this.modal = true;
+ this.stream_interactor = stream_interactor;
+
+ stack.visible = true;
+ stack.vhomogeneous = false;
+ get_content_area().add(stack);
+
+ setup_headerbar();
+ setup_jid_add_view();
+ setup_conference_details_view();
+ show_jid_add_view();
+ }
+
+ private void show_jid_add_view() {
+ cancel_button.remove(cancel_image);
+ cancel_button.add(cancel_label);
+ cancel_button.clicked.disconnect(show_jid_add_view);
+ cancel_button.clicked.connect(close);
+ ok_button.label = "Next";
+ ok_button.sensitive = select_fragment.done;
+ ok_button.clicked.disconnect(on_ok_button_clicked);
+ ok_button.clicked.connect(on_next_button_clicked);
+ details_fragment.notify["done"].disconnect(set_ok_sensitive_from_details);
+ select_fragment.notify["done"].connect(set_ok_sensitive_from_select);
+ stack.transition_type = StackTransitionType.SLIDE_RIGHT;
+ stack.set_visible_child_name("select");
+ }
+
+ private void show_conference_details_view() {
+ cancel_button.remove(cancel_label);
+ cancel_button.add(cancel_image);
+ cancel_button.clicked.disconnect(close);
+ cancel_button.clicked.connect(show_jid_add_view);
+ ok_button.label = "Join";
+ ok_button.sensitive = details_fragment.done;
+ ok_button.clicked.disconnect(on_next_button_clicked);
+ ok_button.clicked.connect(on_ok_button_clicked);
+ select_fragment.notify["done"].disconnect(set_ok_sensitive_from_select);
+ details_fragment.notify["done"].connect(set_ok_sensitive_from_details);
+ stack.transition_type = StackTransitionType.SLIDE_LEFT;
+ stack.set_visible_child_name("details");
+ animate_window_resize();
+ }
+
+ private void setup_headerbar() {
+ HeaderBar header_bar = get_header_bar() as HeaderBar;
+ header_bar.show_close_button = false;
+
+ cancel_button = new Button();
+ header_bar.pack_start(cancel_button);
+ cancel_button.visible = true;
+
+ ok_button = new Button();
+ header_bar.pack_end(ok_button);
+ ok_button.get_style_context().add_class("suggested-action");
+ ok_button.visible = true;
+ ok_button.can_focus = true;
+ ok_button.can_default = true;
+ ok_button.has_default = true;
+ }
+
+ private void setup_jid_add_view() {
+ conference_list = new ConferenceList(stream_interactor);
+ conference_list.row_activated.connect(() => { ok_button.clicked(); });
+ select_fragment = new SelectJidFragment(stream_interactor, conference_list);
+ select_fragment.add_jid.connect((row) => {
+ AddGroupchatDialog dialog = new AddGroupchatDialog(stream_interactor);
+ dialog.set_transient_for(this);
+ dialog.show();
+ });
+ select_fragment.edit_jid.connect((row) => {
+ ConferenceListRow conference_row = row as ConferenceListRow;
+ AddGroupchatDialog dialog = new AddGroupchatDialog.for_conference(stream_interactor, conference_row.account, conference_row.bookmark);
+ dialog.set_transient_for(this);
+ dialog.show();
+ });
+ select_fragment.remove_jid.connect((row) => {
+ ConferenceListRow conference_row = row as ConferenceListRow;
+ MucManager.get_instance(stream_interactor).remove_bookmark(conference_row.account, conference_row.bookmark);
+ });
+ stack.add_named(select_fragment, "select");
+ }
+
+ private void setup_conference_details_view() {
+ details_fragment = new ConferenceDetailsFragment(stream_interactor);
+ stack.add_named(details_fragment, "details");
+ }
+
+ private void set_ok_sensitive_from_select() {
+ ok_button.sensitive = select_fragment.done;
+ }
+
+ private void set_ok_sensitive_from_details() {
+ ok_button.sensitive = select_fragment.done;
+ }
+
+ private void on_next_button_clicked() {
+ details_fragment.clear();
+ ListRow? row = conference_list.get_selected_row() as ListRow;
+ ConferenceListRow? conference_row = conference_list.get_selected_row() as ConferenceListRow;
+ if (conference_row != null) {
+ details_fragment.jid = conference_row.bookmark.jid;
+ details_fragment.nick = conference_row.bookmark.nick;
+ if (conference_row.bookmark.password != null) details_fragment.password = conference_row.bookmark.password;
+ ok_button.grab_focus();
+ } else if (row != null) {
+ details_fragment.jid = row.jid.to_string();
+ details_fragment.set_editable();
+ }
+ show_conference_details_view();
+ }
+
+ private void on_ok_button_clicked() {
+ MucManager.get_instance(stream_interactor).join(details_fragment.account, new Jid(details_fragment.jid), details_fragment.nick, details_fragment.password);
+ close();
+ }
+
+ private void close() {
+ base.close();
+ }
+
+ private void animate_window_resize() {
+ int def_height, curr_width, curr_height;
+ get_size(out curr_width, out curr_height);
+ stack.get_preferred_height(null, out def_height);
+ int difference = def_height - curr_height;
+ Timer timer = new Timer();
+ Timeout.add((int) (stack.transition_duration / 30),
+ () => {
+ ulong microsec;
+ timer.elapsed(out microsec);
+ ulong millisec = microsec / 1000;
+ double partial = double.min(1, (double) millisec / stack.transition_duration);
+ resize(curr_width, (int) (curr_height + difference * partial));
+ return millisec < stack.transition_duration;
+ });
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/list_row.vala b/main/src/ui/add_conversation/list_row.vala
new file mode 100644
index 00000000..b53432a6
--- /dev/null
+++ b/main/src/ui/add_conversation/list_row.vala
@@ -0,0 +1,39 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation {
+
+[GtkTemplate (ui = "/org/dino-im/add_conversation/list_row.ui")]
+public class ListRow : ListBoxRow {
+
+ [GtkChild] public Image image;
+ [GtkChild] public Label name_label;
+ [GtkChild] public Label via_label;
+
+ public Jid? jid;
+ public Account? account;
+
+ public ListRow() {}
+
+ public ListRow.from_jid(StreamInteractor stream_interactor, Jid jid, Account account) {
+ this.jid = jid;
+ this.account = account;
+
+ string display_name = Util.get_display_name(stream_interactor, jid, account);
+ if (stream_interactor.get_accounts().size > 1) {
+ via_label.label = @"via $(account.bare_jid)";
+ this.has_tooltip = true;
+ set_tooltip_text(jid.to_string());
+ } else if (display_name != jid.bare_jid.to_string()){
+ via_label.label = jid.bare_jid.to_string();
+ } else {
+ via_label.visible = false;
+ }
+ name_label.label = display_name;
+ image.set_from_pixbuf((new AvatarGenerator(35, 35)).draw_jid(stream_interactor, jid, account));
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/add_conversation/select_jid_fragment.vala b/main/src/ui/add_conversation/select_jid_fragment.vala
new file mode 100644
index 00000000..d0b214b5
--- /dev/null
+++ b/main/src/ui/add_conversation/select_jid_fragment.vala
@@ -0,0 +1,115 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.AddConversation {
+
+[GtkTemplate (ui = "/org/dino-im/add_conversation/select_jid_fragment.ui")]
+public class SelectJidFragment : Gtk.Box {
+
+ public signal void add_jid();
+ public signal void edit_jid(ListRow row);
+ public signal void remove_jid(ListRow row);
+ public bool done {
+ get {
+ return filterable_list.get_selected_row() != null;
+ }
+ private set {} }
+
+ [GtkChild] private Entry entry;
+ [GtkChild] private Box box;
+ [GtkChild] private Button add_button;
+ [GtkChild] private Button edit_button;
+ [GtkChild] private Button remove_button;
+
+ private FilterableList filterable_list;
+ private ArrayList<AddListRow> added_rows = new ArrayList<AddListRow>();
+ private StreamInteractor stream_interactor;
+
+ public SelectJidFragment(StreamInteractor stream_interactor, FilterableList filterable_list) {
+ this.stream_interactor = stream_interactor;
+ this.filterable_list = filterable_list;
+
+ filterable_list.visible = true;
+ filterable_list.activate_on_single_click = false;
+ filterable_list.vexpand = true;
+ box.add(filterable_list);
+
+ filterable_list.set_sort_func(sort);
+ filterable_list.row_selected.connect(check_buttons_active);
+ filterable_list.row_selected.connect(() => { done = true; }); // just for notifying
+ entry.changed.connect(on_entry_changed);
+ add_button.clicked.connect(() => { add_jid(); });
+ remove_button.clicked.connect(() => { remove_jid(filterable_list.get_selected_row() as ListRow); });
+ edit_button.clicked.connect(() => { edit_jid(filterable_list.get_selected_row() as ListRow); });
+ }
+
+ private void on_entry_changed() {
+ foreach (AddListRow row in added_rows) {
+ filterable_list.remove(row);
+ }
+ added_rows.clear();
+
+ string[] ? values;
+ string str = entry.get_text();
+ values = str == "" ? null : str.split(" ");
+ filterable_list.set_filter_values(values);
+ Jid? parsed_jid = Jid.parse(str);
+ if (parsed_jid != null && parsed_jid.localpart != null) {
+ foreach (Account account in stream_interactor.get_accounts()) {
+ AddListRow row = new AddListRow(stream_interactor, str, account);
+ filterable_list.add(row);
+ added_rows.add(row);
+ }
+ }
+ }
+
+ private void check_buttons_active() {
+ ListBoxRow? row = filterable_list.get_selected_row();
+ bool active = row != null && !row.get_type().is_a(typeof(AddListRow));
+ edit_button.sensitive = active;
+ remove_button.sensitive = active;
+ }
+
+ private int sort(ListBoxRow row1, ListBoxRow row2) {
+ AddListRow al1 = (row1 as AddListRow);
+ AddListRow al2 = (row2 as AddListRow);
+ if (al1 != null && al2 == null) {
+ return -1;
+ } else if (al2 != null && al1 == null) {
+ return 1;
+ }
+ return filterable_list.sort(row1, row2);
+ }
+
+ private class AddListRow : ListRow {
+
+ public AddListRow(StreamInteractor stream_interactor, string jid, Account account) {
+ this.account = account;
+ this.jid = new Jid(jid);
+
+ name_label.label = jid;
+ if (stream_interactor.get_accounts().size > 1) {
+ via_label.label = account.bare_jid.to_string();
+ } else {
+ via_label.visible = false;
+ }
+ image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_greyscale(true).draw_text("?"));
+ }
+ }
+}
+
+public abstract class FilterableList : Gtk.ListBox {
+ public string[]? filter_values;
+
+ public void set_filter_values(string[] values) {
+ if (filter_values == values) return;
+ filter_values = values;
+ invalidate_filter();
+ }
+
+ public abstract int sort(ListBoxRow row1, ListBoxRow row2);
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala
new file mode 100644
index 00000000..1adccf8e
--- /dev/null
+++ b/main/src/ui/application.vala
@@ -0,0 +1,97 @@
+using Gtk;
+
+using Dino.Entities;
+using Dino.Ui;
+
+public class Dino.Ui.Application : Dino.Application {
+ private Notifications notifications;
+ private UnifiedWindow? window;
+ private ConversationSelector.View? filterable_conversation_list;
+ private ConversationSelector.List? conversation_list;
+ private ConversationSummary.View? conversation_frame;
+ private ChatInput? chat_input;
+
+ public Application() {
+ Notify.init("dino");
+ notifications = new Notifications(stream_interaction);
+ notifications.start();
+
+ load_css();
+ }
+
+ public override void activate() {
+ create_set_app_menu();
+ create_window();
+ window.show_all();
+ restore();
+ }
+
+ private void create_window() {
+ window = new UnifiedWindow(this, stream_interaction);
+
+ filterable_conversation_list = window.filterable_conversation_list;
+ conversation_list = window.filterable_conversation_list.conversation_list;
+ conversation_frame = window.conversation_frame;
+ chat_input = window.chat_input;
+ }
+
+ private void show_accounts_window() {
+ ManageAccounts.Dialog dialog = new ManageAccounts.Dialog(stream_interaction, db);
+ dialog.set_transient_for(window);
+ dialog.account_enabled.connect(add_connection);
+ dialog.account_disabled.connect(remove_connection);
+ dialog.show();
+ }
+
+ private void show_settings_window() {
+ SettingsDialog dialog = new SettingsDialog();
+ dialog.set_transient_for(window);
+ dialog.show();
+ }
+
+ private void create_set_app_menu() {
+ SimpleAction accounts_action = new SimpleAction("accounts", null);
+ accounts_action.activate.connect(show_accounts_window);
+ add_action(accounts_action);
+
+ SimpleAction settings_action = new SimpleAction("settings", null);
+ settings_action.activate.connect(show_settings_window);
+ add_action(settings_action);
+
+ SimpleAction quit_action = new SimpleAction("quit", null);
+ quit_action.activate.connect(quit);
+ add_action(quit_action);
+ add_accelerator("<Ctrl>Q", "app.quit", null);
+
+ Builder builder = new Builder.from_resource("/org/dino-im/menu_app.ui");
+ MenuModel menu = builder.get_object("menu_app") as MenuModel;
+
+ set_app_menu(menu);
+ }
+
+ private void restore() {
+ foreach (Account account in db.get_accounts()) {
+ if (account.enabled) add_connection(account);
+ }
+ }
+
+ private void add_connection(Account account) {
+ stream_interaction.connect(account);
+ }
+
+ private void remove_connection(Account account) {
+ stream_interaction.disconnect(account);
+ }
+
+ private void load_css() {
+ var css_provider = new Gtk.CssProvider ();
+ try {
+ var file = File.new_for_uri("resource:///org/dino-im/style.css");
+ css_provider.load_from_file (file);
+ } catch (GLib.Error e) {
+ warning ("loading css: %s", e.message);
+ }
+ Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+}
+
diff --git a/main/src/ui/avatar_generator.vala b/main/src/ui/avatar_generator.vala
new file mode 100644
index 00000000..b668444c
--- /dev/null
+++ b/main/src/ui/avatar_generator.vala
@@ -0,0 +1,235 @@
+using Cairo;
+using Gee;
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+public class AvatarGenerator {
+
+ private const string COLOR_GREY = "E0E0E0";
+ private const string GROUPCHAT_ICON = "system-users-symbolic";
+
+ StreamInteractor? stream_interactor;
+ bool greyscale = false;
+ bool stateless = false;
+ int width;
+ int height;
+ int scale_factor;
+
+ public AvatarGenerator(int width, int height, int scale_factor = 1) {
+ this.width = width;
+ this.height = height;
+ this.scale_factor = scale_factor;
+ }
+
+ public Pixbuf draw_jid(StreamInteractor stream_interactor, Jid jid, Account account) {
+ this.stream_interactor = stream_interactor;
+ return crop_corners(draw_tile(jid, account, width * scale_factor, height * scale_factor));
+ }
+
+ public Pixbuf draw_message(StreamInteractor stream_interactor, Message message) {
+ Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message);
+ return draw_jid(stream_interactor, real_jid != null ? real_jid : message.from, message.account);
+ }
+
+ public Pixbuf draw_conversation(StreamInteractor stream_interactor, Conversation conversation) {
+ return draw_jid(stream_interactor, conversation.counterpart, conversation.account);
+ }
+
+ public Pixbuf draw_account(StreamInteractor stream_interactor, Account account) {
+ return draw_jid(stream_interactor, account.bare_jid, account);
+ }
+
+ public Pixbuf draw_text(string text) {
+ string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(text);
+ Pixbuf pixbuf = draw_colored_rectangle_text(color, text, width, height);
+ return crop_corners(pixbuf);
+ }
+
+ public AvatarGenerator set_greyscale(bool greyscale) {
+ this.greyscale = greyscale;
+ return this;
+ }
+
+ public AvatarGenerator set_stateless(bool stateless) {
+ this.stateless = stateless;
+ return this;
+ }
+
+ private int get_left_border() {
+ return (int)Math.floor(scale_factor/2.0);
+ }
+
+ private int get_right_border() {
+ return (int)Math.ceil(scale_factor/2.0);
+ }
+
+ private void add_tile_to_pixbuf(Pixbuf pixbuf, Jid jid, Account account, int width, int height, int x, int y) {
+ Pixbuf tile = draw_chat_tile(jid, account, width, height);
+ tile.copy_area(0, 0, width, height, pixbuf, x, y);
+ }
+
+ private Pixbuf draw_tile(Jid jid, Account account, int width, int height) {
+ if (MucManager.get_instance(stream_interactor).is_groupchat(jid, account)) {
+ return draw_groupchat_tile(jid, account, width, height);
+ } else {
+ return draw_chat_tile(jid, account, width, height);
+ }
+ }
+
+ private Pixbuf draw_chat_tile(Jid jid, Account account, int width, int height) {
+ if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
+ Jid? real_jid = MucManager.get_instance(stream_interactor).get_real_jid(jid, account);
+ if (real_jid != null) {
+ return draw_tile(real_jid, account, width, height);
+ }
+ }
+ Pixbuf? avatar = AvatarManager.get_instance(stream_interactor).get_avatar(account, jid);
+ if (avatar != null) {
+ double desired_ratio = (double) width / height;
+ double avatar_ratio = (double) avatar.width / avatar.height;
+ if (avatar_ratio > desired_ratio) {
+ int comp_width = width * avatar.height / height;
+ avatar = new Pixbuf.subpixbuf(avatar, avatar.width / 2 - comp_width / 2, 0, comp_width, avatar.height);
+ } else if (avatar_ratio < desired_ratio) {
+ int comp_height = height * avatar.width / width;
+ avatar = new Pixbuf.subpixbuf(avatar, 0, avatar.height / 2 - comp_height / 2, avatar.width, comp_height);
+ }
+ avatar = avatar.scale_simple(width, height, InterpType.BILINEAR);
+ if (greyscale) avatar = convert_to_greyscale(avatar);
+ return avatar;
+ } else {
+ string display_name = Util.get_display_name(stream_interactor, jid, account);
+ string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(display_name);
+ return draw_colored_rectangle_text(color, display_name.get_char(0).toupper().to_string(), width, height);
+ }
+ }
+
+ private Pixbuf draw_groupchat_tile(Jid jid, Account account, int width, int height) {
+ ArrayList<Jid>? occupants = MucManager.get_instance(stream_interactor).get_other_occupants(jid, account);
+ if (stateless || occupants == null || occupants.size == 0) {
+ return draw_chat_tile(jid, account, width, height);
+ }
+ Pixbuf pixbuf = initialize_pixbuf(width, height);
+ if (occupants.size == 1 || occupants.size == 2 || occupants.size == 3) {
+ add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height, 0, 0);
+ if (occupants.size == 1) {
+ add_tile_to_pixbuf(pixbuf, account.bare_jid, account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0);
+ } else if (occupants.size == 2) {
+ add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0);
+ } else if (occupants.size == 3) {
+ add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0);
+ add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border());
+ }
+ } else if (occupants.size >= 4) {
+ add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height / 2 - get_right_border(), 0, 0);
+ add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0);
+ add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_right_border(), height / 2 - get_left_border(), 0, height / 2 + get_left_border());
+ if (occupants.size == 4) {
+ add_tile_to_pixbuf(pixbuf, occupants[3], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border());
+ } else if (occupants.size > 4) {
+ Pixbuf plus_pixbuf = draw_colored_rectangle_text("555753", "+", width / 2 - get_left_border(), height / 2 - get_left_border());
+ if (greyscale) plus_pixbuf = convert_to_greyscale(plus_pixbuf);
+ plus_pixbuf.copy_area(0, 0, width / 2 - get_left_border(), height / 2 - get_left_border(), pixbuf, width / 2 + get_left_border(), height / 2 + get_left_border());
+ }
+ }
+ return pixbuf;
+ }
+
+ public Pixbuf draw_colored_icon(string hex_color, string icon, int width, int height) {
+ int ICON_SIZE = width > 20 * scale_factor ? 17 * scale_factor : 14 * scale_factor;
+
+ Context rectancle_context = new Context(new ImageSurface(Format.ARGB32, width, height));
+ draw_colored_rectangle(rectancle_context, hex_color, width, height);
+
+ Pixbuf icon_pixbuf = IconTheme.get_default().load_icon(icon, ICON_SIZE, IconLookupFlags.FORCE_SIZE);
+ Surface icon_surface = cairo_surface_create_from_pixbuf(icon_pixbuf, 1, null);
+ Context context = new Context(icon_surface);
+ context.set_operator(Operator.IN);
+ context.set_source_rgba(1, 1, 1, 1);
+ context.rectangle(0, 0, width, height);
+ context.fill();
+
+ rectancle_context.set_source_surface(icon_surface, width / 2 - ICON_SIZE / 2, height / 2 - ICON_SIZE / 2);
+ rectancle_context.paint();
+
+ return pixbuf_get_from_surface(rectancle_context.get_target(), 0, 0, width, height);
+ }
+
+ public Pixbuf draw_colored_rectangle_text(string hex_color, string text, int width, int height) {
+ Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height));
+ draw_colored_rectangle(ctx, hex_color, width, height);
+ draw_center_text(ctx, text, width < 40 * scale_factor ? 17 * scale_factor : 25 * scale_factor, width, height);
+ return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height);
+ }
+
+ private static void draw_center_text(Context ctx, string text, int fontsize, int width, int height) {
+ ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ ctx.set_font_size(fontsize);
+ Cairo.TextExtents extents;
+ ctx.text_extents(text, out extents);
+ double x_pos = width/2 - (extents.width/2 + extents.x_bearing);
+ double y_pos = height/2 - (extents.height/2 + extents.y_bearing);
+ ctx.move_to(x_pos, y_pos);
+ ctx.set_source_rgba(1, 1, 1, 1);
+ ctx.show_text(text);
+ }
+
+ private static void draw_colored_rectangle(Context ctx, string hex_color, int width, int height) {
+ set_source_hex_color(ctx, hex_color);
+ ctx.rectangle(0, 0, width, height);
+ ctx.fill();
+ }
+
+ private static Pixbuf convert_to_greyscale(Pixbuf pixbuf) {
+ Surface surface = cairo_surface_create_from_pixbuf(pixbuf, 1, null);
+ Context context = new Context(surface);
+ // convert to greyscale
+ context.set_operator(Operator.HSL_COLOR);
+ context.set_source_rgb(1, 1, 1);
+ context.rectangle(0, 0, pixbuf.width, pixbuf.height);
+ context.fill();
+ // make the visible part more light
+ context.set_operator(Operator.ATOP);
+ context.set_source_rgba(1, 1, 1, 0.7);
+ context.rectangle(0, 0, pixbuf.width, pixbuf.height);
+ context.fill();
+ return pixbuf_get_from_surface(context.get_target(), 0, 0, pixbuf.width, pixbuf.height);
+ }
+
+ private Pixbuf crop_corners(Pixbuf pixbuf, double radius = 3) {
+ radius *= scale_factor;
+ Context ctx = new Context(new ImageSurface(Format.ARGB32, pixbuf.width, pixbuf.height));
+ cairo_set_source_pixbuf(ctx, pixbuf, 0, 0);
+ double degrees = Math.PI / 180.0;
+ ctx.new_sub_path();
+ ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees);
+ ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees);
+ ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees);
+ ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees);
+ ctx.close_path();
+ ctx.clip();
+ ctx.paint();
+ return pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height);
+ }
+
+ private static Pixbuf initialize_pixbuf(int width, int height) {
+ Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height));
+ ctx.set_source_rgba(1, 1, 1, 0);
+ ctx.rectangle(0, 0, width, height);
+ ctx.fill();
+ return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height);
+ }
+
+ private static void set_source_hex_color(Context ctx, string hex_color) {
+ ctx.set_source_rgba((double) hex_color.substring(0, 2).to_long(null, 16) / 255,
+ (double) hex_color.substring(2, 2).to_long(null, 16) / 255,
+ (double) hex_color.substring(4, 2).to_long(null, 16) / 255,
+ hex_color.length > 6 ? (double) hex_color.substring(6, 2).to_long(null, 16) / 255 : 1);
+ }
+}
+
+}
diff --git a/main/src/ui/chat_input.vala b/main/src/ui/chat_input.vala
new file mode 100644
index 00000000..5030fcea
--- /dev/null
+++ b/main/src/ui/chat_input.vala
@@ -0,0 +1,119 @@
+using Gdk;
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/org/dino-im/chat_input.ui")]
+public class ChatInput : Grid {
+
+ [GtkChild] private TextView text_input;
+
+ private Conversation? conversation;
+ private StreamInteractor stream_interactor;
+ private HashMap<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func);
+ private static HashMap<string, string> smiley_translations = new HashMap<string, string>();
+
+ static construct {
+ smiley_translations[":)"] = "🙂";
+ smiley_translations[":D"] = "😀";
+ smiley_translations[";)"] = "😉";
+ smiley_translations["O:)"] = "😇";
+ smiley_translations["]:>"] = "😈";
+ smiley_translations[":o"] = "😮";
+ smiley_translations[":P"] = "😛";
+ smiley_translations[";P"] = "😜";
+ smiley_translations[":("] = "🙁";
+ smiley_translations[":'("] = "😢";
+ smiley_translations[":/"] = "😕";
+ smiley_translations["-.-"] = "😑";
+ }
+
+ public ChatInput(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public void initialize_for_conversation(Conversation conversation) {
+ if (this.conversation != null) {
+ if (text_input.buffer.text != "") {
+ entry_cache[this.conversation] = text_input.buffer.text;
+ } else {
+ entry_cache.unset(this.conversation);
+ }
+ }
+ this.conversation = conversation;
+ text_input.buffer.text = "";
+ if (entry_cache.has_key(conversation)) {
+ text_input.buffer.text = entry_cache[conversation];
+ }
+ text_input.key_press_event.connect(on_text_input_key_press);
+ text_input.key_release_event.connect(on_text_input_key_release);
+ text_input.grab_focus();
+ }
+
+ private void send_text() {
+ string text = text_input.buffer.text;
+ if (text.has_prefix("/")) {
+ string[] token = text.split(" ", 2);
+ switch(token[0]) {
+ case "/kick":
+ MucManager.get_instance(stream_interactor).kick(conversation.account, conversation.counterpart, token[1]);
+ break;
+ case "/me":
+ MessageManager.get_instance(stream_interactor).send_message(text, conversation);
+ break;
+ case "/nick":
+ MucManager.get_instance(stream_interactor).change_nick(conversation.account, conversation.counterpart, token[1]);
+ break;
+ case "/topic":
+ MucManager.get_instance(stream_interactor).change_subject(conversation.account, conversation.counterpart, token[1]);
+ break;
+ }
+ } else {
+ MessageManager.get_instance(stream_interactor).send_message(text, conversation);
+ }
+ text_input.buffer.text = "";
+ }
+
+ private bool on_text_input_key_press(EventKey event) {
+ if (event.keyval == Key.space || event.keyval == Key.Return) {
+ check_convert_smiley();
+ }
+ if (event.keyval == Key.Return) {
+ if (event.state == ModifierType.SHIFT_MASK) {
+ text_input.buffer.insert_at_cursor("\n", 1);
+ } else if (text_input.buffer.text != ""){
+ send_text();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void check_convert_smiley() {
+ if (Dino.Settings.instance().convert_utf8_smileys) {
+ foreach (string smiley in smiley_translations.keys) {
+ if (text_input.buffer.text.has_suffix(smiley)) {
+ if (text_input.buffer.text.length == smiley.length ||
+ text_input.buffer.text[text_input.buffer.text.length - smiley.length - 1] == ' ') {
+ text_input.buffer.text = text_input.buffer.text.substring(0, text_input.buffer.text.length - smiley.length) + smiley_translations[smiley];
+ }
+ }
+ }
+ }
+ }
+
+ private bool on_text_input_key_release(EventKey event) {
+ if (text_input.buffer.text != "") {
+ ChatInteraction.get_instance(stream_interactor).on_message_entered(conversation);
+ } else {
+ ChatInteraction.get_instance(stream_interactor).on_message_cleared(conversation);
+ }
+ return false;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_list_titlebar.vala b/main/src/ui/conversation_list_titlebar.vala
new file mode 100644
index 00000000..5ef824d5
--- /dev/null
+++ b/main/src/ui/conversation_list_titlebar.vala
@@ -0,0 +1,47 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_list_titlebar.ui")]
+public class ConversationListTitlebar : Gtk.HeaderBar {
+
+ public signal void conversation_opened(Conversation conversation);
+
+ [GtkChild] private MenuButton add_button;
+ [GtkChild] public ToggleButton search_button;
+
+ private StreamInteractor stream_interactor;
+
+ public ConversationListTitlebar(Window application, StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ create_add_menu(application);
+ }
+
+ private void create_add_menu(Window window) {
+ SimpleAction contacts_action = new SimpleAction("add_chat", null);
+ contacts_action.activate.connect(() => {
+ AddConversation.Chat.Dialog add_chat_dialog = new AddConversation.Chat.Dialog(stream_interactor);
+ add_chat_dialog.set_transient_for((Window) get_toplevel());
+ add_chat_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation));
+ add_chat_dialog.show();
+ });
+ window.get_application().add_action(contacts_action);
+
+ SimpleAction conference_action = new SimpleAction("add_conference", null);
+ conference_action.activate.connect(() => {
+ AddConversation.Conference.Dialog add_conference_dialog = new AddConversation.Conference.Dialog(stream_interactor);
+ add_conference_dialog.set_transient_for((Window) get_toplevel());
+ add_conference_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation));
+ add_conference_dialog.show();
+ });
+ window.get_application().add_action(conference_action);
+
+ Builder builder = new Builder.from_resource("/org/dino-im/menu_add.ui");
+ MenuModel menu = builder.get_object("menu_add") as MenuModel;
+ add_button.set_menu_model(menu);
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_selector/chat_row.vala b/main/src/ui/conversation_selector/chat_row.vala
new file mode 100644
index 00000000..8b36b333
--- /dev/null
+++ b/main/src/ui/conversation_selector/chat_row.vala
@@ -0,0 +1,90 @@
+using Gdk;
+using Gee;
+using Gtk;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSelector {
+
+public class ChatRow : ConversationRow {
+
+ public ChatRow(StreamInteractor stream_interactor, Conversation conversation) {
+ base(stream_interactor, conversation);
+ has_tooltip = true;
+ query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => {
+ tooltip.set_custom(generate_tooltip());
+ return true;
+ });
+ update_avatar();
+ }
+
+ public override void on_show_received(Show show) {
+ update_avatar();
+ }
+
+ public override void network_connection(bool connected) {
+ if (!connected) {
+ set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)).set_greyscale(true).draw_conversation(stream_interactor, conversation), image.scale_factor);
+ } else {
+ update_avatar();
+ }
+ }
+
+ public void on_updated_roster_item(Roster.Item roster_item) {
+ if (roster_item.name != null) {
+ display_name = roster_item.name;
+ update_name();
+ }
+ update_avatar();
+ }
+
+ public void update_avatar() {
+ ArrayList<Jid> full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account);
+ set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor))
+ .set_greyscale(full_jids == null)
+ .draw_conversation(stream_interactor, conversation), image.scale_factor);
+ }
+
+ private Widget generate_tooltip() {
+ Builder builder = new Builder.from_resource("/org/dino-im/conversation_selector/chat_row_tooltip.ui");
+ Box main_box = builder.get_object("main_box") as Box;
+ Box inner_box = builder.get_object("inner_box") as Box;
+ Label jid_label = builder.get_object("jid_label") as Label;
+
+ jid_label.label = conversation.counterpart.to_string();
+
+ ArrayList<Jid>? full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account);
+ if (full_jids != null) {
+ for (int i = 0; i < full_jids.size; i++) {
+ Box box = new Box(Orientation.HORIZONTAL, 5);
+
+ Show show = PresenceManager.get_instance(stream_interactor).get_last_show(full_jids[i], conversation.account);
+ Image image = new Image();
+ Pixbuf pixbuf;
+ int icon_size = 13 * image.scale_factor;
+ if (show.as == Show.AWAY) {
+ pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_away.svg", icon_size, icon_size, true);
+ } else if (show.as == Show.XA || show.as == Show.DND) {
+ pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_dnd.svg", icon_size, icon_size, true);
+ } else if (show.as == Show.CHAT) {
+ pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_chat.svg", icon_size, icon_size, true);
+ } else {
+ pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_online.svg", icon_size, icon_size, true);
+ }
+ Util.image_set_from_scaled_pixbuf(image, pixbuf);
+ box.add(image);
+
+ Label resource = new Label(full_jids[i].resourcepart);
+ resource.xalign = 0;
+ box.add(resource);
+ box.show_all();
+
+ inner_box.add(box);
+ }
+ }
+ return main_box;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_selector/conversation_row.vala b/main/src/ui/conversation_selector/conversation_row.vala
new file mode 100644
index 00000000..0a6b7e70
--- /dev/null
+++ b/main/src/ui/conversation_selector/conversation_row.vala
@@ -0,0 +1,160 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Pango;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSelector {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_selector/conversation_row.ui")]
+public abstract class ConversationRow : ListBoxRow {
+
+ [GtkChild] protected Image image;
+ [GtkChild] private Label name_label;
+ [GtkChild] private Label time_label;
+ [GtkChild] private Label message_label;
+ [GtkChild] protected Button x_button;
+ [GtkChild] private Revealer time_revealer;
+ [GtkChild] private Revealer xbutton_revealer;
+ [GtkChild] public Revealer main_revealer;
+
+ public Conversation conversation { get; private set; }
+
+ protected const int AVATAR_SIZE = 40;
+
+ protected string display_name;
+ protected string message;
+ protected DateTime time;
+ protected bool read = true;
+
+
+ protected StreamInteractor stream_interactor;
+
+ construct {
+ name_label.attributes = new AttrList();
+ }
+
+ public ConversationRow(StreamInteractor stream_interactor, Conversation conversation) {
+ this.conversation = conversation;
+ this.stream_interactor = stream_interactor;
+
+ x_button.clicked.connect(on_x_button_clicked);
+
+ update_name(Util.get_conversation_display_name(stream_interactor, conversation));
+ Entities.Message message = MessageManager.get_instance(stream_interactor).get_last_message(conversation);
+ if (message != null) {
+ message_received(message);
+ }
+ }
+
+ public void update() {
+ update_time();
+ }
+
+ public void message_received(Entities.Message message) {
+ update_message(message.body.replace("\n", " "));
+ update_time(message.time.to_local());
+ }
+
+ public void set_avatar(Pixbuf pixbuf, int scale_factor = 1) {
+ Util.image_set_from_scaled_pixbuf(image, pixbuf, scale_factor);
+ image.queue_draw();
+ }
+
+ public void mark_read() {
+ update_read(true);
+ }
+
+ public void mark_unread() {
+ update_read(false);
+ }
+
+ public abstract void on_show_received(Show presence);
+ public abstract void network_connection(bool connected);
+
+ protected void update_name(string? new_name = null) {
+ if (new_name != null) {
+ display_name = new_name;
+ }
+ name_label.label = display_name;
+ }
+
+ protected void update_time(DateTime? new_time = null) {
+ time_label.visible = true;
+ if (new_time != null) {
+ time = new_time;
+ }
+ if (time != null) {
+ time_label.label = get_relative_time(time);
+ }
+ }
+
+ protected void update_message(string? new_message = null) {
+ if (new_message != null) {
+ message = new_message;
+ }
+ if (message != null) {
+ message_label.visible = true;
+ message_label.label = message;
+ }
+ }
+
+ protected void update_read(bool read) {
+ this.read = read;
+ if (read) {
+ name_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD)));
+ time_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD)));
+ message_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD)));
+ } else {
+ name_label.attributes.insert(attr_weight_new(Weight.BOLD));
+ time_label.attributes.insert(attr_weight_new(Weight.BOLD));
+ message_label.attributes.insert(attr_weight_new(Weight.BOLD));
+ }
+ name_label.label = name_label.label; // TODO initializes redrawing, which would otherwise not happen. nicer?
+ time_label.label = time_label.label;
+ message_label.label = message_label.label;
+ }
+
+ private void on_x_button_clicked() {
+ main_revealer.set_transition_type(RevealerTransitionType.SLIDE_UP);
+ main_revealer.set_reveal_child(false);
+ main_revealer.notify["child-revealed"].connect(() => {
+ conversation.active = false;
+ });
+ }
+
+ public override void state_flags_changed(StateFlags flags) {
+ StateFlags curr_flags = get_state_flags();
+ if ((curr_flags & StateFlags.PRELIGHT) != 0) {
+ time_revealer.set_reveal_child(false);
+ xbutton_revealer.set_reveal_child(true);
+ } else {
+ time_revealer.set_reveal_child(true);
+ xbutton_revealer.set_reveal_child(false);
+ }
+ }
+
+ private static string get_relative_time(DateTime datetime) {
+ DateTime now = new DateTime.now_local();
+ TimeSpan timespan = now.difference(datetime);
+ if (timespan > 365 * TimeSpan.DAY) {
+ return datetime.get_year().to_string();
+ } else if (timespan > 7 * TimeSpan.DAY) {
+ return datetime.format("%d.%m");
+ } else if (timespan > 2 * TimeSpan.DAY) {
+ return datetime.format("%a");
+ } else if (timespan > 1 * TimeSpan.DAY) {
+ return "Yesterday";
+ } else if (timespan > 9 * TimeSpan.MINUTE) {
+ return datetime.format("%H:%M");
+ } else if (timespan > 1 * TimeSpan.MINUTE) {
+ return (timespan / TimeSpan.MINUTE).to_string() + " min ago";
+ } else {
+ return "Just now";
+ }
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_selector/groupchat_row.vala b/main/src/ui/conversation_selector/groupchat_row.vala
new file mode 100644
index 00000000..7fe52d89
--- /dev/null
+++ b/main/src/ui/conversation_selector/groupchat_row.vala
@@ -0,0 +1,35 @@
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSelector {
+
+public class GroupchatRow : ConversationRow {
+
+ public GroupchatRow(StreamInteractor stream_interactor, Conversation conversation) {
+ base(stream_interactor, conversation);
+ has_tooltip = true;
+ set_tooltip_text(conversation.counterpart.bare_jid.to_string());
+ set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor))
+ .set_greyscale(true)
+ .draw_conversation(stream_interactor, conversation), image.scale_factor);
+ x_button.clicked.connect(on_x_button_clicked);
+ }
+
+
+ public override void on_show_received(Show show) {
+ set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor))
+ .draw_conversation(stream_interactor, conversation), image.scale_factor);
+ }
+
+ public override void network_connection(bool connected) {
+ set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor))
+ .set_greyscale(!connected ||
+ MucManager.get_instance(stream_interactor).get_nick(conversation.counterpart, conversation.account) == null) // TODO better currently joined
+ .draw_conversation(stream_interactor, conversation), image.scale_factor);
+ }
+
+ private void on_x_button_clicked() {
+ MucManager.get_instance(stream_interactor).part(conversation.account, conversation.counterpart);
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_selector/list.vala b/main/src/ui/conversation_selector/list.vala
new file mode 100644
index 00000000..ea2f9622
--- /dev/null
+++ b/main/src/ui/conversation_selector/list.vala
@@ -0,0 +1,175 @@
+using Gee;
+using Gtk;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSelector {
+
+public class List : ListBox {
+
+ public signal void conversation_selected(Conversation conversation);
+
+ private StreamInteractor stream_interactor;
+ private string[]? filter_values;
+ private HashMap<Conversation, ConversationRow> rows = new HashMap<Conversation, ConversationRow>(Conversation.hash_func, Conversation.equals_func);
+
+ public List(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ get_style_context().add_class("sidebar");
+ set_filter_func(filter);
+ set_header_func(header);
+ set_sort_func(sort);
+
+ ChatInteraction.get_instance(stream_interactor).conversation_read.connect((conversation) => {
+ Idle.add(() => {rows[conversation].mark_read(); return false;});
+ });
+ ChatInteraction.get_instance(stream_interactor).conversation_unread.connect((conversation) => {
+ Idle.add(() => {rows[conversation].mark_unread(); return false;});
+ });
+ ConversationManager.get_instance(stream_interactor).conversation_activated.connect((conversation) => {
+ Idle.add(() => {add_conversation(conversation); return false;});
+ });
+ MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => {
+ Idle.add(() => {message_received(message, conversation); return false;});
+ });
+ MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => {
+ Idle.add(() => {message_received(message, conversation); return false;});
+ });
+ PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => {
+ Idle.add(() => {
+ Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
+ if (conversation != null && rows.has_key(conversation)) rows[conversation].on_show_received(show);
+ return false;
+ });
+ });
+ RosterManager.get_instance(stream_interactor).updated_roster_item.connect((account, jid, roster_item) => {
+ Idle.add(() => {
+ Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
+ if (conversation != null && rows.has_key(conversation)) {
+ ChatRow row = rows[conversation] as ChatRow;
+ if (row != null) row.on_updated_roster_item(roster_item);
+ }
+ return false;
+ });
+ });
+ AvatarManager.get_instance(stream_interactor).received_avatar.connect((avatar, jid, account) => {
+ Idle.add(() => {
+ Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
+ if (conversation != null && rows.has_key(conversation)) {
+ ChatRow row = rows[conversation] as ChatRow;
+ if (row != null) row.update_avatar();
+ }
+ return false;
+ });
+ });
+ stream_interactor.connection_manager.connection_state_changed.connect((account, state) => {
+ Idle.add(() => {
+ foreach (ConversationRow row in rows.values) {
+ if (row.conversation.account.equals(account)) row.network_connection(state == ConnectionManager.ConnectionState.CONNECTED);
+ }
+ return false;
+ });
+ });
+ Timeout.add_seconds(60, () => {
+ foreach (ConversationRow row in rows.values) row.update();
+ return true;
+ });
+ }
+
+ public override void row_activated(ListBoxRow r) {
+ if (r.get_type().is_a(typeof(ConversationRow))) {
+ ConversationRow row = r as ConversationRow;
+ conversation_selected(row.conversation);
+ }
+ }
+
+ public void set_filter_values(string[]? values) {
+ if (filter_values == values) {
+ return;
+ }
+ filter_values = values;
+ invalidate_filter();
+ }
+
+ public void add_conversation(Conversation conversation) {
+ ConversationRow row;
+ if (!rows.has_key(conversation)) {
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ row = new GroupchatRow(stream_interactor, conversation);
+ } else {
+ row = new ChatRow(stream_interactor, conversation);
+ }
+ rows[conversation] = row;
+ add(row);
+ row.main_revealer.set_reveal_child(true);
+ conversation.notify["active"].connect((s, p) => {
+ if (rows.has_key(conversation) && !conversation.active) {
+ remove_conversation(conversation);
+ }
+ });
+ }
+ invalidate_sort();
+ queue_draw();
+ }
+
+ public void remove_conversation(Conversation conversation) {
+ remove(rows[conversation]);
+ rows.unset(conversation);
+ }
+
+ public void on_conversation_selected(Conversation conversation) {
+ if (!rows.has_key(conversation)) {
+ add_conversation(conversation);
+ }
+ this.select_row(rows[conversation]);
+ }
+
+ private void message_received(Entities.Message message, Conversation conversation) {
+ if (rows.has_key(conversation)) {
+ rows[conversation].message_received(message);
+ invalidate_sort();
+ }
+ }
+
+ private void header(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+ private bool filter(ListBoxRow r) {
+ if (r.get_type().is_a(typeof(ConversationRow))) {
+ ConversationRow row = r as ConversationRow;
+ if (filter_values != null && filter_values.length != 0) {
+ foreach (string filter in filter_values) {
+ if (!(Util.get_conversation_display_name(stream_interactor, row.conversation).down().contains(filter.down()) ||
+ row.conversation.counterpart.to_string().down().contains(filter.down()))) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ private int sort(ListBoxRow row1, ListBoxRow row2) {
+ ConversationRow cr1 = row1 as ConversationRow;
+ ConversationRow cr2 = row2 as ConversationRow;
+ if (cr1 != null && cr2 != null) {
+ Conversation c1 = cr1.conversation;
+ Conversation c2 = cr2.conversation;
+ int comp = c2.last_active.compare(c1.last_active);
+ if (comp == 0) {
+ return Util.get_conversation_display_name(stream_interactor, c1)
+ .collate(Util.get_conversation_display_name(stream_interactor, c2));
+ } else {
+ return comp;
+ }
+ }
+ return 0;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_selector/view.vala b/main/src/ui/conversation_selector/view.vala
new file mode 100644
index 00000000..ae641664
--- /dev/null
+++ b/main/src/ui/conversation_selector/view.vala
@@ -0,0 +1,52 @@
+using Gee;
+using Gtk;
+using Gdk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSelector {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_selector/view.ui")]
+public class View : Grid {
+ public List conversation_list;
+
+ [GtkChild] public SearchEntry search_entry;
+ [GtkChild] public SearchBar search_bar;
+ [GtkChild] private ScrolledWindow scrolled;
+
+ public View(StreamInteractor stream_interactor) {
+ conversation_list = new List(stream_interactor);
+ scrolled.add(conversation_list);
+ search_entry.key_release_event.connect(search_key_release_event);
+ search_entry.search_changed.connect(search_changed);
+ }
+
+ public void conversation_selected(Conversation? conversation) {
+ search_entry.set_text("");
+ }
+
+ private void refilter() {
+ string[]? values = null;
+ string str = search_entry.get_text ();
+ if (str != "") values = str.split(" ");
+ conversation_list.set_filter_values(values);
+ }
+
+ private void search_changed(Editable editable) {
+ refilter();
+ }
+
+ private bool search_key_release_event(EventKey event) {
+ conversation_list.select_row(conversation_list.get_row_at_y(0));
+ if (event.keyval == Key.Down) {
+ ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0);
+ if (row != null) {
+ conversation_list.select_row(row);
+ row.grab_focus();
+ }
+ }
+ return false;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_summary/merged_message_item.vala b/main/src/ui/conversation_summary/merged_message_item.vala
new file mode 100644
index 00000000..fa198d21
--- /dev/null
+++ b/main/src/ui/conversation_summary/merged_message_item.vala
@@ -0,0 +1,159 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Markup;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")]
+public class MergedMessageItem : Grid {
+
+ public Conversation conversation { get; set; }
+ public Jid from { get; private set; }
+ public DateTime initial_time { get; private set; }
+ public ArrayList<Message> messages = new ArrayList<Message>(Message.equals_func);
+
+ [GtkChild] private Image image;
+ [GtkChild] private Label time_label;
+ [GtkChild] private Label name_label;
+ [GtkChild] private Image encryption_image;
+ [GtkChild] private Image received_image;
+ [GtkChild] private TextView message_text_view;
+
+ public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
+ this.conversation = conversation;
+ this.from = message.from;
+ this.initial_time = message.time;
+ setup_tags();
+ add_message(message);
+
+ time_label.label = get_relative_time(initial_time.to_local());
+ string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+ name_label.set_markup(@"<span foreground=\"#$(Util.get_name_hex_color(display_name))\">$display_name</span>");
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
+ if (message.encryption != Encryption.NONE) {
+ encryption_image.visible = true;
+ encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
+ }
+ }
+
+ public void update() {
+ time_label.label = get_relative_time(initial_time.to_local());
+ }
+
+ public void add_message(Message message) {
+ TextIter end;
+ message_text_view.buffer.get_end_iter(out end);
+ if (messages.size > 0) {
+ message_text_view.buffer.insert(ref end, "\n", -1);
+ }
+ message_text_view.buffer.insert(ref end, message.body, -1);
+ format_suffix_urls(message.body);
+ messages.add(message);
+ message.notify["marked"].connect_after(update_received); // TODO other thread? not main? css error? gtk main?
+ update_received();
+ }
+
+ private void update_received() {
+ bool all_received = true;
+ bool all_read = true;
+ foreach (Message message in messages) {
+ if (message.marked == Message.Marked.WONTSEND) {
+ received_image.visible = true;
+ Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default();
+ Gtk.IconInfo? icon_info = icon_theme.lookup_icon("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR, 0);
+ received_image.set_from_pixbuf(icon_info.load_symbolic({1,0,0,1}));
+ return;
+ } else if (message.marked != Message.Marked.READ) {
+ all_read = false;
+ if (message.marked != Message.Marked.RECEIVED) {
+ all_received = false;
+ }
+ }
+ }
+ if (all_read) {
+ received_image.visible = true;
+ received_image.set_from_resource("/org/dino-im/img/double_tick.svg");
+ } else if (all_received) {
+ received_image.visible = true;
+ received_image.set_from_resource("/org/dino-im/img/tick.svg");
+ } else if (received_image.visible) {
+ received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
+ }
+ }
+
+ private void format_suffix_urls(string text) {
+ int absolute_start = message_text_view.buffer.text.length - text.length;
+
+ Regex url_regex = new Regex("""(?i)\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""");
+ MatchInfo match_info;
+ url_regex.match(text, 0, out match_info);
+ for (; match_info.matches(); match_info.next()) {
+ string? url = match_info.fetch(0);
+ int start;
+ int end;
+ match_info.fetch_pos(0, out start, out end);
+ TextIter start_iter;
+ TextIter end_iter;
+ message_text_view.buffer.get_iter_at_offset(out start_iter, absolute_start + start);
+ message_text_view.buffer.get_iter_at_offset(out end_iter, absolute_start + end);
+ message_text_view.buffer.apply_tag_by_name("url", start_iter, end_iter);
+ }
+ }
+
+ private void setup_tags() {
+ message_text_view.buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue");
+ message_text_view.button_release_event.connect(open_url);
+ message_text_view.motion_notify_event.connect(change_cursor_over_url);
+ }
+
+ private bool open_url(EventButton event_button) {
+ int buffer_x, buffer_y;
+ message_text_view.window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y);
+ TextIter iter;
+ message_text_view.get_iter_at_location(out iter, buffer_x, buffer_y);
+ TextIter start_iter = iter, end_iter = iter;
+ if (start_iter.backward_to_tag_toggle(null) && end_iter.forward_to_tag_toggle(null)) {
+ string url = start_iter.get_text(end_iter);
+ try{
+ AppInfo.launch_default_for_uri(url, null);
+ } catch (Error err) {
+ print("Tryed to open " + url);
+ }
+ }
+ return false;
+ }
+
+ private bool change_cursor_over_url(EventMotion event_motion) {
+ TextIter iter;
+ message_text_view.get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y);
+ if (iter.has_tag(message_text_view.buffer.tag_table.lookup("url"))) {
+ event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.HAND2));
+ } else {
+ event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.XTERM));
+ }
+ return false;
+ }
+
+ private static string get_relative_time(DateTime datetime) {
+ DateTime now = new DateTime.now_local();
+ TimeSpan timespan = now.difference(datetime);
+ if (timespan > 365 * TimeSpan.DAY) {
+ return datetime.format("%d.%m.%Y %H:%M");
+ } else if (timespan > 7 * TimeSpan.DAY) {
+ return datetime.format("%d.%m %H:%M");
+ } else if (timespan > 1 * TimeSpan.DAY) {
+ return datetime.format("%a, %H:%M");
+ } else if (timespan > 9 * TimeSpan.MINUTE) {
+ return datetime.format("%H:%M");
+ } else if (timespan > TimeSpan.MINUTE) {
+ return (timespan / TimeSpan.MINUTE).to_string() + " min ago";
+ } else {
+ return "Just now";
+ }
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/merged_status_item.vala b/main/src/ui/conversation_summary/merged_status_item.vala
new file mode 100644
index 00000000..1fe8ecf3
--- /dev/null
+++ b/main/src/ui/conversation_summary/merged_status_item.vala
@@ -0,0 +1,31 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+private class MergedStatusItem : Expander {
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+ private ArrayList<Show> statuses = new ArrayList<Show>();
+
+ public MergedStatusItem(StreamInteractor stream_interactor, Conversation conversation, Show show) {
+ set_hexpand(true);
+ add_status(show);
+ }
+
+ public void add_status(Show show) {
+ statuses.add(show);
+ StatusItem status_item = new StatusItem(stream_interactor, conversation, @"is $(show.as)");
+ if (statuses.size == 1) {
+ label = show.as;
+ } else {
+ label = @"changed their status $(statuses.size) times";
+ add(new Label(show.as));
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_summary/status_item.vala b/main/src/ui/conversation_summary/status_item.vala
new file mode 100644
index 00000000..0775d8f3
--- /dev/null
+++ b/main/src/ui/conversation_summary/status_item.vala
@@ -0,0 +1,30 @@
+using Gtk;
+using Markup;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+private class StatusItem : Grid {
+
+ private Image image = new Image();
+ private Label label = new Label("");
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+
+ public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) {
+ Object(column_spacing : 7);
+ set_hexpand(true);
+ this.stream_interactor = stream_interactor;
+ this.conversation = conversation;
+ image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation));
+ attach(image, 0, 0, 1, 1);
+ attach(label, 1, 0, 1, 1);
+ string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account);
+ label.set_markup(@"<span foreground=\"#B1B1B1\"> $(escape_text(display_name)) $text </span>");
+ show_all();
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_summary/view.vala b/main/src/ui/conversation_summary/view.vala
new file mode 100644
index 00000000..d2599041
--- /dev/null
+++ b/main/src/ui/conversation_summary/view.vala
@@ -0,0 +1,220 @@
+using Gee;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui.ConversationSummary {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_summary/view.ui")]
+public class View : Box {
+
+ public Conversation? conversation { get; private set; }
+ public HashMap<Entities.Message, MergedMessageItem> message_items = new HashMap<Entities.Message, MergedMessageItem>(Entities.Message.hash_func, Entities.Message.equals_func);
+
+ [GtkChild] private ScrolledWindow scrolled;
+ [GtkChild] private Box main;
+
+ private StreamInteractor stream_interactor;
+ private MergedMessageItem? last_message_item;
+ private StatusItem typing_status;
+ private Entities.Message? earliest_message;
+ double? was_value;
+ double? was_upper;
+ double? was_page_size;
+ Object reloading_lock = new Object();
+ bool reloading = false;
+
+ public View(StreamInteractor stream_interactor) {
+ Object(homogeneous : false, spacing : 0);
+ this.stream_interactor = stream_interactor;
+ scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
+ scrolled.vadjustment.notify["value"].connect(on_value_notify);
+
+ CounterpartInteractionManager.get_instance(stream_interactor).received_state.connect((account, jid, state) => {
+ Idle.add(() => { on_received_state(account, jid, state); return false; });
+ });
+ MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => {
+ Idle.add(() => { show_message(message, conversation, true); return false; });
+ });
+ MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => {
+ Idle.add(() => { show_message(message, conversation, true); return false; });
+ });
+ PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => {
+ Idle.add(() => { on_show_received(show, jid, account); return false; });
+ });
+ Timeout.add_seconds(60, () => {
+ foreach (MergedMessageItem message_item in message_items.values) {
+ message_item.update();
+ }
+ return true;
+ });
+ }
+
+ public void initialize_for_conversation(Conversation? conversation) {
+ this.conversation = conversation;
+ clear();
+ message_items.clear();
+ was_upper = null;
+ was_page_size = null;
+ last_message_item = null;
+
+ ArrayList<Object> objects = new ArrayList<Object>();
+ Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
+ if (messages != null && messages.size > 0) {
+ earliest_message = messages[0];
+ objects.add_all(messages);
+ }
+ HashMap<Jid, ArrayList<Show>>? shows = PresenceManager.get_instance(stream_interactor).get_shows(conversation.counterpart, conversation.account);
+ if (shows != null) {
+ foreach (Jid jid in shows.keys) objects.add_all(shows[jid]);
+ }
+ objects.sort((a, b) => {
+ DateTime? dt1 = null;
+ DateTime? dt2 = null;
+ Entities.Message m1 = a as Entities.Message;
+ if (m1 != null) dt1 = m1.time;
+ Show s1 = a as Show;
+ if (s1 != null) dt1 = s1.datetime;
+ Entities.Message m2 = b as Entities.Message;
+ if (m2 != null) dt2 = m2.time;
+ Show s2 = b as Show;
+ if (s2 != null) dt2 = s2.datetime;
+ return dt1.compare(dt2);
+ });
+ foreach (Object o in objects) {
+ Entities.Message message = o as Entities.Message;
+ Show show = o as Show;
+ if (message != null) {
+ show_message(message, conversation);
+ } else if (show != null) {
+ on_show_received(show, conversation.counterpart, conversation.account);
+ }
+ }
+ update_chat_state();
+ }
+
+ private void on_received_state(Account account, Jid jid, string state) {
+ if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) {
+ update_chat_state(state);
+ }
+ }
+
+ private void update_chat_state(string? state = null) {
+ string? state_ = state;
+ if (state_ == null) {
+ state_ = CounterpartInteractionManager.get_instance(stream_interactor).get_chat_state(conversation.account, conversation.counterpart);
+ }
+ if (typing_status != null) {
+ main.remove(typing_status);
+ }
+ if (state_ != null) {
+ if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
+ if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) {
+ typing_status = new StatusItem(stream_interactor, conversation, "is typing...");
+ } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
+ typing_status = new StatusItem(stream_interactor, conversation, "has stoped typing");
+ }
+ main.add(typing_status);
+ }
+ }
+ }
+
+ private void on_show_received(Show show, Jid jid, Account account) {
+
+ }
+
+ private void on_upper_notify() {
+ if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 ||
+ scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
+ scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
+ } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1){
+ scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
+ }
+ was_upper = scrolled.vadjustment.upper;
+ was_page_size = scrolled.vadjustment.page_size;
+ lock(reloading_lock) {
+ reloading = false;
+ }
+ }
+
+ private void on_value_notify() {
+ if (scrolled.vadjustment.value < 200) {
+ load_earlier_messages();
+ }
+ }
+
+ private void load_earlier_messages() {
+ was_value = scrolled.vadjustment.value;
+ lock(reloading_lock) {
+ if(reloading) return;
+ reloading = true;
+ }
+ Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages_before(conversation, earliest_message);
+ if (messages != null && messages.size > 0) {
+ earliest_message = messages[0];
+ MergedMessageItem? current_item = null;
+ int items_added = 0;
+ for (int i = 0; i < messages.size; i++) {
+ if (current_item != null && should_merge_message(current_item, messages[i])) {
+ current_item.add_message(messages[i]);
+ } else {
+ current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]);
+ force_alloc_width(current_item, main.get_allocated_width());
+ main.add(current_item);
+ message_items[messages[i]] = current_item;
+ main.reorder_child(current_item, items_added);
+ items_added++;
+ }
+ }
+ return;
+ }
+ reloading = false;
+ }
+
+ private void show_message(Entities.Message message, Conversation conversation, bool animate = false) {
+ if (this.conversation != null && this.conversation.equals(conversation)) {
+ if (should_merge_message(last_message_item, message)) {
+ last_message_item.add_message(message);
+ } else {
+ MergedMessageItem message_item = new MergedMessageItem(stream_interactor, conversation, message);
+ if (animate) {
+ Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
+ revealer.add(message_item);
+ force_alloc_width(revealer, main.get_allocated_width());
+ main.add(revealer);
+ revealer.set_reveal_child(true);
+ } else {
+ force_alloc_width(message_item, main.get_allocated_width());
+ main.add(message_item);
+ }
+ last_message_item = message_item;
+ }
+ message_items[message] = last_message_item;
+ update_chat_state();
+ }
+ }
+
+ private bool should_merge_message(MergedMessageItem? message_item, Entities.Message message) {
+ return message_item != null &&
+ message_item.from.equals(message.from) &&
+ message_item.messages.get(0).encryption == message.encryption &&
+ message.time.difference(message_item.initial_time) < TimeSpan.MINUTE &&
+ (message_item.messages.get(0).marked == Entities.Message.Marked.WONTSEND) == (message.marked == Entities.Message.Marked.WONTSEND);
+ }
+
+ private void force_alloc_width(Widget widget, int width) {
+ Allocation alloc = Allocation();
+ widget.get_preferred_width(out alloc.width, null);
+ widget.get_preferred_height(out alloc.height, null);
+ alloc.width = width;
+ widget.size_allocate(alloc);
+ }
+
+ private void clear() {
+ main.@foreach((widget) => { main.remove(widget); });
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_titlebar.vala b/main/src/ui/conversation_titlebar.vala
new file mode 100644
index 00000000..4f472e05
--- /dev/null
+++ b/main/src/ui/conversation_titlebar.vala
@@ -0,0 +1,131 @@
+using Gtk;
+using Gee;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/org/dino-im/conversation_titlebar.ui")]
+public class ConversationTitlebar : Gtk.HeaderBar {
+
+ [GtkChild] private MenuButton menu_button;
+ [GtkChild] private MenuButton encryption_button;
+ [GtkChild] private MenuButton groupchat_button;
+
+ private RadioButton? button_unencrypted;
+ private Map<RadioButton, Plugins.EncryptionListEntry> encryption_radios = new HashMap<RadioButton, Plugins.EncryptionListEntry>();
+
+ private StreamInteractor stream_interactor;
+ private Conversation? conversation;
+
+ public ConversationTitlebar(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ MucManager.get_instance(stream_interactor).groupchat_subject_set.connect((account, jid, subject) => {
+ Idle.add(() => { on_groupchat_subject_set(account, jid, subject); return false; });
+ });
+ create_conversation_menu();
+ create_encryption_menu();
+ }
+
+ public void initialize_for_conversation(Conversation conversation) {
+ this.conversation = conversation;
+ update_encryption_menu_state();
+ update_encryption_menu_icon();
+ update_groupchat_menu();
+ update_title();
+ update_subtitle();
+ }
+
+ private void update_encryption_menu_state() {
+ foreach (RadioButton e in encryption_radios.keys) {
+ e.set_sensitive(encryption_radios[e].can_encrypt(conversation));
+ if (conversation.encryption == encryption_radios[e].encryption) e.set_active(true);
+ }
+ if (conversation.encryption == Encryption.NONE) {
+ button_unencrypted.set_active(true);
+ }
+ }
+
+ private void update_encryption_menu_icon() {
+ encryption_button.visible = (conversation.type_ == Conversation.Type.CHAT);
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ if (conversation.encryption == Encryption.NONE) {
+ encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON));
+ } else {
+ encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON));
+ }
+ }
+ }
+
+ private void update_groupchat_menu() {
+ groupchat_button.visible = conversation.type_ == Conversation.Type.GROUPCHAT;
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ groupchat_button.set_use_popover(true);
+ Popover popover = new Popover(null);
+ OccupantList occupant_list = new OccupantList(stream_interactor, conversation);
+ popover.add(occupant_list);
+ occupant_list.show_all();
+ groupchat_button.set_popover(popover);
+ }
+ }
+
+ private void update_title() {
+ set_title(Util.get_conversation_display_name(stream_interactor, conversation));
+ }
+
+ private void update_subtitle(string? subtitle = null) {
+ if (subtitle != null) {
+ set_subtitle(subtitle);
+ } else if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ string subject = MucManager.get_instance(stream_interactor).get_groupchat_subject(conversation.counterpart, conversation.account);
+ set_subtitle(subject != "" ? subject : null);
+ } else {
+ set_subtitle(null);
+ }
+ }
+
+ private void create_conversation_menu() {
+ Builder builder = new Builder.from_resource("/org/dino-im/menu_conversation.ui");
+ MenuModel menu = builder.get_object("menu_conversation") as MenuModel;
+ menu_button.set_menu_model(menu);
+ }
+
+ private void encryption_changed() {
+ foreach (RadioButton e in encryption_radios.keys) {
+ if (e.get_active()) {
+ conversation.encryption = encryption_radios[e].encryption;
+ update_encryption_menu_icon();
+ return;
+ }
+ }
+ conversation.encryption = Encryption.NONE;
+ update_encryption_menu_icon();
+ }
+
+ private void create_encryption_menu() {
+ Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui");
+ PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu;
+ Box encryption_box = builder.get_object("encryption_box") as Box;
+ button_unencrypted = builder.get_object("button_unencrypted") as RadioButton;
+ button_unencrypted.toggled.connect(encryption_changed);
+ Application app = GLib.Application.get_default() as Application;
+ foreach(var e in app.plugin_registry.encryption_list_entries) {
+ RadioButton btn = new RadioButton.with_label(button_unencrypted.get_group(), e.name);
+ encryption_radios[btn] = e;
+ btn.toggled.connect(encryption_changed);
+ btn.visible = true;
+ encryption_box.pack_end(btn, false);
+ }
+ encryption_button.set_use_popover(true);
+ encryption_button.set_popover(menu);
+ encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON));
+ }
+
+ private void on_groupchat_subject_set(Account account, Jid jid, string subject) {
+ if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) {
+ update_subtitle(subject);
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/manage_accounts/account_row.vala b/main/src/ui/manage_accounts/account_row.vala
new file mode 100644
index 00000000..5e4570f0
--- /dev/null
+++ b/main/src/ui/manage_accounts/account_row.vala
@@ -0,0 +1,22 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ManageAccounts {
+
+[GtkTemplate (ui = "/org/dino-im/manage_accounts/account_row.ui")]
+public class AccountRow : Gtk.ListBoxRow {
+
+ [GtkChild] public Image image;
+ [GtkChild] public Label jid_label;
+
+ public Account account;
+
+ public AccountRow(StreamInteractor stream_interactor, Account account) {
+ this.account = account;
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(40, 40, image.scale_factor)).draw_account(stream_interactor, account));
+ jid_label.set_label(account.bare_jid.to_string());
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/manage_accounts/add_account_dialog.vala b/main/src/ui/manage_accounts/add_account_dialog.vala
new file mode 100644
index 00000000..f6ad40a2
--- /dev/null
+++ b/main/src/ui/manage_accounts/add_account_dialog.vala
@@ -0,0 +1,62 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ManageAccounts {
+
+[GtkTemplate (ui = "/org/dino-im/manage_accounts/add_account_dialog.ui")]
+public class AddAccountDialog : Gtk.Dialog {
+
+ public signal void added(Account account);
+
+ [GtkChild] private Button cancel_button;
+ [GtkChild] private Button ok_button;
+ [GtkChild] private Entry alias_entry;
+ [GtkChild] private Entry jid_entry;
+ [GtkChild] private Entry password_entry;
+
+ public AddAccountDialog(StreamInteractor stream_interactor) {
+ Object(use_header_bar : 1);
+ this.title = "Add Account";
+
+ cancel_button.clicked.connect(() => { close(); });
+ ok_button.clicked.connect(on_ok_button_clicked);
+ jid_entry.changed.connect(on_jid_entry_changed);
+ jid_entry.focus_out_event.connect(on_jid_entry_focus_out_event);
+ }
+
+ private void on_jid_entry_changed() {
+ Jid? jid = Jid.parse(jid_entry.text);
+ if (jid != null && jid.localpart != null && jid.resourcepart == null) {
+ ok_button.set_sensitive(true);
+ jid_entry.secondary_icon_name = null;
+ } else {
+ ok_button.set_sensitive(false);
+ }
+ }
+
+ private bool on_jid_entry_focus_out_event() {
+ Jid? jid = Jid.parse(jid_entry.text);
+ if (jid == null || jid.localpart == null || jid.resourcepart != null) {
+ jid_entry.secondary_icon_name = "dialog-warning-symbolic";
+ // TODO why doesn't the tooltip work
+ jid_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY, "JID should be of the form \"user@example.com\"");
+ } else {
+ jid_entry.secondary_icon_name = null;
+ }
+ return false;
+ }
+
+ private void on_ok_button_clicked() {
+ Account account = new Account.from_bare_jid(jid_entry.get_text());
+ account.resourcepart = "dino";
+ account.alias = alias_entry.get_text();
+ account.enabled = false;
+ account.password = password_entry.get_text();
+ added(account);
+ close();
+ }
+}
+
+}
diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala
new file mode 100644
index 00000000..3616c403
--- /dev/null
+++ b/main/src/ui/manage_accounts/dialog.vala
@@ -0,0 +1,230 @@
+using Gdk;
+using Gee;
+using Gtk;
+using Markup;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ManageAccounts {
+
+[GtkTemplate (ui = "/org/dino-im/manage_accounts/dialog.ui")]
+public class Dialog : Gtk.Window {
+
+ public signal void account_enabled(Account account);
+ public signal void account_disabled(Account account);
+
+ [GtkChild] public Stack main_stack;
+ [GtkChild] public ListBox account_list;
+ [GtkChild] public Button no_accounts_add;
+ [GtkChild] public ToolButton add_button;
+ [GtkChild] public ToolButton remove_button;
+ [GtkChild] public Image image;
+ [GtkChild] public Button image_button;
+ [GtkChild] public Label jid_label;
+ [GtkChild] public Switch active_switch;
+
+ [GtkChild] public Stack password_stack;
+ [GtkChild] public Label password_label;
+ [GtkChild] public Button password_button;
+ [GtkChild] public Entry password_entry;
+
+ [GtkChild] public Stack alias_stack;
+ [GtkChild] public Label alias_label;
+ [GtkChild] public Button alias_button;
+ [GtkChild] public Entry alias_entry;
+
+ [GtkChild] public Grid settings_list;
+
+ private ArrayList<Plugins.AccountSettingsWidget> plugin_widgets = new ArrayList<Plugins.AccountSettingsWidget>();
+
+ private Database db;
+ private StreamInteractor stream_interactor;
+
+ construct {
+ account_list.row_selected.connect(account_list_row_selected);
+ add_button.clicked.connect(add_button_clicked);
+ no_accounts_add.clicked.connect(add_button_clicked);
+ remove_button.clicked.connect(remove_button_clicked);
+ password_entry.key_release_event.connect(on_password_key_release_event);
+ alias_entry.key_release_event.connect(on_alias_key_release_event);
+ image_button.clicked.connect(on_image_button_clicked);
+
+ main_stack.set_visible_child_name("no_accounts");
+
+ int row_index = 4;
+ int16 default_top_padding = new Gtk.Button().get_style_context().get_padding(Gtk.StateFlags.NORMAL).top + 1;
+ Application app = GLib.Application.get_default() as Application;
+ foreach (var e in app.plugin_registry.account_settings_entries) {
+ Plugins.AccountSettingsWidget widget = e.get_widget();
+ plugin_widgets.add(widget);
+ widget.visible = true;
+ widget.activated.connect(child_activated);
+ Label label = new Label(e.name);
+ label.get_style_context().add_class("dim-label");
+ label.set_padding(0, e.label_top_padding == -1 ? default_top_padding : e.label_top_padding);
+ label.yalign = 0;
+ label.xalign = 1;
+ label.visible = true;
+ settings_list.attach(label, 0, row_index);
+ settings_list.attach(widget, 1, row_index, 2);
+ row_index++;
+ }
+ }
+
+ public Dialog(StreamInteractor stream_interactor, Database db) {
+ this.db = db;
+ this.stream_interactor = stream_interactor;
+ foreach (Account account in db.get_accounts()) {
+ add_account(account);
+ }
+
+ AvatarManager.get_instance(stream_interactor).received_avatar.connect((pixbuf, jid, account) => {
+ Idle.add(() => {
+ on_received_avatar(pixbuf, jid, account);
+ return false;
+ });});
+
+ if (account_list.get_row_at_index(0) != null) account_list.select_row(account_list.get_row_at_index(0));
+ }
+
+ public AccountRow add_account(Account account) {
+ AccountRow account_item = new AccountRow (stream_interactor, account);
+ account_list.add(account_item);
+ main_stack.set_visible_child_name("accounts_exist");
+ return account_item;
+ }
+
+ private void add_button_clicked() {
+ AddAccountDialog add_account_dialog = new AddAccountDialog(stream_interactor);
+ add_account_dialog.set_transient_for(this);
+ add_account_dialog.added.connect((account) => {
+ db.add_account(account);
+ AccountRow account_item = add_account(account);
+ account_list.select_row(account_item);
+ account_list.queue_draw();
+ });
+ add_account_dialog.show();
+ }
+
+ private void remove_button_clicked() {
+ AccountRow account_item = account_list.get_selected_row() as AccountRow;
+ if (account_item != null) {
+ account_list.remove(account_item);
+ account_list.queue_draw();
+ if (account_item.account.enabled) account_disabled(account_item.account);
+ db.remove_account(account_item.account);
+ if (account_list.get_row_at_index(0) != null) {
+ account_list.select_row(account_list.get_row_at_index(0));
+ } else {
+ main_stack.set_visible_child_name("no_accounts");
+ }
+ }
+ }
+
+ private void account_list_row_selected(ListBoxRow? row) {
+ AccountRow? account_item = row as AccountRow;
+ if (account_item != null) populate_grid_data(account_item.account);
+ }
+
+ private void populate_grid_data(Account account) {
+ active_switch.state_set.disconnect(on_active_switch_state_changed);
+
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account));
+ active_switch.set_active(account.enabled);
+ jid_label.label = account.bare_jid.to_string();
+
+ string filler = "";
+ for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string();
+ password_label.label = filler;
+ password_stack.set_visible_child_name("label");
+ password_entry.text = account.password;
+
+ alias_stack.set_visible_child_name("label");
+ alias_label.label = account.alias;
+ alias_entry.text = account.alias;
+
+ password_button.clicked.connect(() => { set_active_stack(password_stack); });
+ alias_button.clicked.connect(() => { set_active_stack(alias_stack); });
+ active_switch.state_set.connect(on_active_switch_state_changed);
+
+ foreach(Plugins.AccountSettingsWidget widget in plugin_widgets) {
+ widget.set_account(account);
+ }
+
+ child_activated(null);
+ }
+
+
+ private void on_image_button_clicked() {
+ FileChooserDialog chooser = new FileChooserDialog (
+ "Select avatar", this, FileChooserAction.OPEN,
+ "Cancel", ResponseType.CANCEL,
+ "Select", ResponseType.ACCEPT);
+ FileFilter filter = new FileFilter();
+ filter.add_mime_type("image/*");
+ chooser.set_filter(filter);
+ if (chooser.run() == Gtk.ResponseType.ACCEPT) {
+ string uri = chooser.get_filename();
+ Account account = (account_list.get_selected_row() as AccountRow).account;
+ AvatarManager.get_instance(stream_interactor).publish(account, uri);
+ }
+ chooser.close();
+ }
+
+ private bool on_active_switch_state_changed(bool state) {
+ Account account = (account_list.get_selected_row() as AccountRow).account;
+ account.enabled = state;
+ if (state) {
+ account_enabled(account);
+ } else {
+ account_disabled(account);
+ }
+ return false;
+ }
+
+ private bool on_password_key_release_event(EventKey event) {
+ Account account = (account_list.get_selected_row() as AccountRow).account;
+ account.password = password_entry.text;
+ string filler = "";
+ for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string();
+ password_label.label = filler;
+ if (event.keyval == Key.Return) {
+ password_stack.set_visible_child_name("label");
+ }
+ return false;
+ }
+
+ private bool on_alias_key_release_event(EventKey event) {
+ Account account = (account_list.get_selected_row() as AccountRow).account;
+ account.alias = alias_entry.text;
+ alias_label.label = alias_entry.text;
+ if (event.keyval == Key.Return) {
+ alias_stack.set_visible_child_name("label");
+ }
+ return false;
+ }
+
+ private void on_received_avatar(Pixbuf pixbuf, Jid jid, Account account) {
+ Account curr_account = (account_list.get_selected_row() as AccountRow).account;
+ if (curr_account.equals(account) && jid.equals(account.bare_jid)) {
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account));
+ }
+ }
+
+ private void child_activated(Gtk.Widget? widget) {
+ if (widget != password_stack) password_stack.set_visible_child_name("label");
+ if (widget != alias_stack) alias_stack.set_visible_child_name("label");
+
+ foreach(var w in plugin_widgets) {
+ if (widget != (Gtk.Widget)w) w.deactivate();
+ }
+ }
+
+ private void set_active_stack(Stack stack) {
+ stack.set_visible_child_name("entry");
+ child_activated(stack);
+ }
+}
+
+}
+
diff --git a/main/src/ui/notifications.vala b/main/src/ui/notifications.vala
new file mode 100644
index 00000000..17636995
--- /dev/null
+++ b/main/src/ui/notifications.vala
@@ -0,0 +1,57 @@
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui {
+
+public class Notifications : GLib.Object {
+
+ private StreamInteractor stream_interactor;
+ private Notify.Notification notification = new Notify.Notification("", null, null);
+
+ public Notifications(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public void start() {
+ MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received);
+ PresenceManager.get_instance(stream_interactor).received_subscription_request.connect(on_received_subscription_request);
+ }
+
+ private void on_message_received(Entities.Message message, Conversation conversation) {
+ if (!ChatInteraction.get_instance(stream_interactor).is_active_focus()) {
+ string display_name = Util.get_conversation_display_name(stream_interactor, conversation);
+ if (MucManager.get_instance(stream_interactor).is_groupchat(conversation.counterpart, conversation.account)) {
+ string muc_occupant = Util.get_display_name(stream_interactor, message.from, conversation.account);
+ display_name = muc_occupant + " in " + display_name;
+ }
+ notification.update(display_name, message.body, null);
+ notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_conversation(stream_interactor, conversation));
+ notification.set_timeout(3);
+ try {
+ notification.show();
+ } catch (Error error) { }
+ }
+ }
+
+ private void on_received_subscription_request(Jid jid, Account account) {
+ Notify.Notification notification = new Notify.Notification("Subscription request", jid.bare_jid.to_string(), null);
+ notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_jid(stream_interactor, jid, account));
+ notification.add_action("accept", "Accept", () => {
+ PresenceManager.get_instance(stream_interactor).approve_subscription(account, jid);
+ try {
+ notification.close();
+ } catch (Error error) { }
+ });
+ notification.add_action("deny", "Deny", () => {
+ PresenceManager.get_instance(stream_interactor).deny_subscription(account, jid);
+ try {
+ notification.close();
+ } catch (Error error) { }
+ });
+ try {
+ notification.show();
+ } catch (Error error) { }
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/occupant_list.vala b/main/src/ui/occupant_list.vala
new file mode 100644
index 00000000..ba7c01d1
--- /dev/null
+++ b/main/src/ui/occupant_list.vala
@@ -0,0 +1,111 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui{
+
+[GtkTemplate (ui = "/org/dino-im/occupant_list.ui")]
+public class OccupantList : Box {
+
+ public signal void conversation_selected(Conversation? conversation);
+ private StreamInteractor stream_interactor;
+
+ [GtkChild] private ListBox list_box;
+ [GtkChild] private SearchEntry search_entry;
+
+ private Conversation? conversation;
+ private string[]? filter_values;
+ private HashMap<Jid, OccupantListRow> rows = new HashMap<Jid, OccupantListRow>(Jid.hash_func, Jid.equals_func);
+
+ public OccupantList(StreamInteractor stream_interactor, Conversation conversation) {
+ this.stream_interactor = stream_interactor;
+ list_box.set_header_func(header);
+ list_box.set_sort_func(sort);
+ list_box.set_filter_func(filter);
+ search_entry.search_changed.connect(search_changed);
+
+ PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => {
+ Idle.add(() => { on_show_received(show, jid, account); return false; });
+ });
+ RosterManager.get_instance(stream_interactor).updated_roster_item.connect(on_updated_roster_item);
+
+ initialize_for_conversation(conversation);
+ }
+
+ public void initialize_for_conversation(Conversation conversation) {
+ this.conversation = conversation;
+ ArrayList<Jid>? occupants = MucManager.get_instance(stream_interactor).get_occupants(conversation.counterpart, conversation.account);
+ if (occupants != null) {
+ foreach (Jid occupant in occupants) {
+ add_occupant(occupant);
+ }
+ }
+ }
+
+ private void refilter() {
+ string[]? values = null;
+ string str = search_entry.get_text ();
+ if (str != "") values = str.split(" ");
+ if (filter_values == values) return;
+ filter_values = values;
+ list_box.invalidate_filter();
+ }
+
+ private void search_changed(Editable editable) {
+ refilter();
+ }
+
+ public void add_occupant(Jid jid) {
+ rows[jid] = new OccupantListRow(stream_interactor, conversation.account, jid);
+ list_box.add(rows[jid]);
+ list_box.invalidate_filter();
+ list_box.invalidate_sort();
+ }
+
+ public void remove_occupant(Jid jid) {
+ list_box.remove(rows[jid]);
+ rows.unset(jid);
+ }
+
+ private void on_updated_roster_item(Account account, Jid jid, Xmpp.Roster.Item roster_item) {
+
+ }
+
+ private void on_show_received(Show show, Jid jid, Account account) {
+ if (conversation != null && conversation.counterpart.equals_bare(jid)) {
+ if (show.as == Show.OFFLINE && rows.has_key(jid)) {
+ remove_occupant(jid);
+ } else if (show.as != Show.OFFLINE && !rows.has_key(jid)) {
+ add_occupant(jid);
+ }
+ }
+ }
+
+ private void header(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+ private bool filter(ListBoxRow r) {
+ if (r.get_type().is_a(typeof(OccupantListRow))) {
+ OccupantListRow row = r as OccupantListRow;
+ foreach (string filter in filter_values) {
+ return row.name_label.label.down().contains(filter.down());
+ }
+ }
+ return true;
+ }
+
+ private int sort(ListBoxRow row1, ListBoxRow row2) {
+ if (row1.get_type().is_a(typeof(OccupantListRow)) && row2.get_type().is_a(typeof(OccupantListRow))) {
+ OccupantListRow c1 = row1 as OccupantListRow;
+ OccupantListRow c2 = row2 as OccupantListRow;
+ return c1.name_label.label.collate(c2.name_label.label);
+ }
+ return 0;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/occupant_list_row.vala b/main/src/ui/occupant_list_row.vala
new file mode 100644
index 00000000..4dc7c0c5
--- /dev/null
+++ b/main/src/ui/occupant_list_row.vala
@@ -0,0 +1,25 @@
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/org/dino-im/occupant_list_item.ui")]
+public class OccupantListRow : ListBoxRow {
+
+ [GtkChild] private Image image;
+ [GtkChild] public Label name_label;
+
+ public OccupantListRow(StreamInteractor stream_interactor, Account account, Jid jid) {
+ name_label.label = Util.get_display_name(stream_interactor, jid, account);
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_jid(stream_interactor, jid, account));
+ //has_tooltip = true;
+ }
+
+ public void on_presence_received(Presence.Stanza presence) {
+
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/settings_dialog.vala b/main/src/ui/settings_dialog.vala
new file mode 100644
index 00000000..2d3c1efa
--- /dev/null
+++ b/main/src/ui/settings_dialog.vala
@@ -0,0 +1,24 @@
+using Gtk;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/org/dino-im/settings_dialog.ui")]
+class SettingsDialog : Dialog {
+
+ [GtkChild] private CheckButton marker_checkbutton;
+ [GtkChild] private CheckButton emoji_checkbutton;
+
+ Dino.Settings settings = Dino.Settings.instance();
+
+ public SettingsDialog() {
+ Object(use_header_bar : 1);
+
+ marker_checkbutton.active = settings.send_read;
+ emoji_checkbutton.active = settings.convert_utf8_smileys;
+
+ marker_checkbutton.toggled.connect(() => { settings.send_read = marker_checkbutton.active; });
+ emoji_checkbutton.toggled.connect(() => { settings.convert_utf8_smileys = emoji_checkbutton.active; });
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala
new file mode 100644
index 00000000..269799c2
--- /dev/null
+++ b/main/src/ui/unified_window.vala
@@ -0,0 +1,81 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+public class UnifiedWindow : Window {
+ public ChatInput chat_input;
+ public ConversationListTitlebar conversation_list_titlebar;
+ public ConversationSelector.View filterable_conversation_list;
+ public ConversationSummary.View conversation_frame;
+ public ConversationTitlebar conversation_titlebar;
+ public Paned paned;
+
+ private StreamInteractor stream_interactor;
+ private Conversation? conversation;
+
+ public UnifiedWindow(Application application, StreamInteractor stream_interactor) {
+ Object(application : application);
+ this.stream_interactor = stream_interactor;
+ focus_in_event.connect(on_focus_in_event);
+ focus_out_event.connect(on_focus_out_event);
+
+ default_width = 1200;
+ default_height = 700;
+
+ chat_input = new ChatInput(stream_interactor);
+ conversation_frame = new ConversationSummary.View(stream_interactor);
+ conversation_titlebar = new ConversationTitlebar(stream_interactor);
+ paned = new Paned(Orientation.HORIZONTAL);
+ paned.set_position(300);
+ filterable_conversation_list = new ConversationSelector.View(stream_interactor);
+ conversation_list_titlebar = new ConversationListTitlebar(this, stream_interactor);
+ conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_bar, "search-mode-enabled",
+ BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+ Grid grid = new Grid();
+ grid.orientation = Orientation.VERTICAL;
+ Paned toolbar_paned = new Paned(Orientation.HORIZONTAL);
+
+ add(paned);
+ paned.add1(filterable_conversation_list);
+ paned.add2(grid);
+
+ grid.add(conversation_frame);
+ grid.add(new Separator(Orientation.HORIZONTAL));
+ grid.add(chat_input);
+
+ conversation_frame.show_all();
+
+ toolbar_paned.add1(conversation_list_titlebar);
+ toolbar_paned.add2(conversation_titlebar);
+ paned.bind_property("position", toolbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+ set_titlebar(toolbar_paned);
+
+ filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected);
+ conversation_list_titlebar.conversation_opened.connect(on_conversation_selected);
+ }
+
+ private void on_conversation_selected(Conversation conversation) {
+ this.conversation = conversation;
+ ChatInteraction.get_instance(stream_interactor).on_conversation_selected(conversation);
+ conversation.active = true; // only for conversation_selected
+ filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened
+
+ chat_input.initialize_for_conversation(conversation);
+ conversation_frame.initialize_for_conversation(conversation);
+ conversation_titlebar.initialize_for_conversation(conversation);
+ }
+
+ private bool on_focus_in_event() {
+ ChatInteraction.get_instance(stream_interactor).window_focus_in(conversation);
+ return false;
+ }
+
+ private bool on_focus_out_event() {
+ ChatInteraction.get_instance(stream_interactor).window_focus_out(conversation);
+ return false;
+ }
+}
+
+} \ No newline at end of file
diff --git a/main/src/ui/util.vala b/main/src/ui/util.vala
new file mode 100644
index 00000000..0a294731
--- /dev/null
+++ b/main/src/ui/util.vala
@@ -0,0 +1,75 @@
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui {
+
+public class Util : Object {
+
+ private const string[] tango_colors_light = {"FCE94F", "FCAF3E", "E9B96E", "8AE234", "729FCF", "AD7FA8", "EF2929"};
+ private const string[] tango_colors_medium = {"EDD400", "F57900", "C17D11", "73D216", "3465A4", "75507B", "CC0000"};
+ private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673AB7", "3f51B5", "2196F3", "03A9f4", "00BCD4", "009688", "4CAF50", "8BC34a", "CDDC39", "FFEB3B", "FFC107", "FF9800", "FF5722", "795548"};
+ private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFF176", "FFD54F", "FFB74D", "FF8A65", "A1887F"};
+ private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFF59D", "FFE082", "FFCC80", "FFAB91", "BCAAA4"};
+
+ public static string get_avatar_hex_color(string name) {
+ return material_colors_300[name.hash() % material_colors_300.length];
+// return tango_colors_light[name.hash() % tango_colors_light.length];
+ }
+
+ public static string get_name_hex_color(string name) {
+ return material_colors_500[name.hash() % material_colors_500.length];
+// return tango_colors_medium[name.hash() % tango_colors_medium.length];
+ }
+
+ public static string color_for_show(string show) {
+ switch(show) {
+ case "online": return "#9CCC65";
+ case "away": return "#FFCA28";
+ case "chat": return "#66BB6A";
+ case "xa": return "#EF5350";
+ case "dnd": return "#EF5350";
+ default: return "#BDBDBD";
+ }
+ }
+
+ public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) {
+ return get_display_name(stream_interactor, conversation.counterpart, conversation.account);
+ }
+
+ public static string get_display_name(StreamInteractor stream_interactor, Jid jid, Account account) {
+ if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
+ return jid.resourcepart;
+ } else {
+ if (jid.bare_jid.equals(account.bare_jid.bare_jid)) {
+ if (account.alias == null || account.alias == "") {
+ return account.bare_jid.to_string();
+ } else {
+ return account.alias;
+ }
+ }
+ Roster.Item roster_item = RosterManager.get_instance(stream_interactor).get_roster_item(account, jid);
+ if (roster_item != null && roster_item.name != null) {
+ return roster_item.name;
+ }
+ return jid.bare_jid.to_string();
+ }
+ }
+
+ public static string get_message_display_name(StreamInteractor stream_interactor, Entities.Message message, Account account) {
+ Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message);
+ if (real_jid != null) {
+ return get_display_name(stream_interactor, real_jid, account);
+ } else {
+ return get_display_name(stream_interactor, message.from, account);
+ }
+ }
+
+ public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0) {
+ if (scale == 0) scale = image.get_scale_factor();
+ image.set_from_surface(Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window()));
+ }
+}
+
+} \ No newline at end of file