Initial commit
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
*.o
|
||||
build/
|
||||
Makefile
|
||||
.vscode/
|
||||
*.iml
|
||||
.idea
|
||||
.sqlite3
|
25
CMakeLists.txt
Normal file
|
@ -0,0 +1,25 @@
|
|||
list(APPEND CMAKE_MODULE_PATH
|
||||
${CMAKE_SOURCE_DIR}/cmake
|
||||
)
|
||||
|
||||
include(CheckCCompilerFlag)
|
||||
macro(AddCFlagIfSupported flag test)
|
||||
CHECK_C_COMPILER_FLAG(${flag} ${test})
|
||||
if(${${test}})
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
|
||||
if("Ninja" STREQUAL ${CMAKE_GENERATOR})
|
||||
AddCFlagIfSupported(-fdiagnostics-color COMPILER_SUPPORTS_fdiagnistics-color)
|
||||
endif()
|
||||
|
||||
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
set (VALA_CFLAGS -Wno-deprecated-declarations -Wno-incompatible-pointer-types -Wno-int-conversion)
|
||||
|
||||
add_subdirectory(qlite)
|
||||
add_subdirectory(vala-xmpp)
|
||||
add_subdirectory(client)
|
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
#Dino
|
||||
![screenshots](http://i.imgur.com/n9caTuJ.png)
|
||||
|
||||
##Build
|
||||
./configure
|
||||
make
|
||||
glib-compile-schemas client/data
|
||||
env GSETTINGS_SCHEMA_DIR=client/data/ build/dino
|
150
client/CMakeLists.txt
Normal file
|
@ -0,0 +1,150 @@
|
|||
find_package(Vala REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(GPGME REQUIRED)
|
||||
find_package(LIBUUID REQUIRED)
|
||||
include(${VALA_USE_FILE})
|
||||
include(GlibCompileResourcesSupport)
|
||||
|
||||
set(CLIENT_PACKAGES
|
||||
gee-0.8
|
||||
gio-2.0
|
||||
glib-2.0
|
||||
gtk+-3.0
|
||||
libnotify
|
||||
sqlite3
|
||||
)
|
||||
|
||||
pkg_check_modules(CLIENT REQUIRED ${CLIENT_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(
|
||||
CLIENT_GRESOURCES_TARGET
|
||||
CLIENT_GRESOURCES_XML
|
||||
TARGET ${CMAKE_BINARY_DIR}/resources/resources.c
|
||||
TYPE EMBED_C
|
||||
RESOURCES ${RESOURCE_LIST}
|
||||
PREFIX /org/dino-im
|
||||
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data
|
||||
)
|
||||
|
||||
vala_precompile(CLIENT_VALA_C
|
||||
SOURCES
|
||||
src/main.vala
|
||||
|
||||
src/dbus/login1.vala
|
||||
src/dbus/networkmanager.vala
|
||||
src/dbus/upower.vala
|
||||
|
||||
src/entity/account.vala
|
||||
src/entity/conversation.vala
|
||||
src/entity/jid.vala
|
||||
src/entity/message.vala
|
||||
|
||||
src/service/avatar_manager.vala
|
||||
src/service/avatar_storage.vala
|
||||
src/service/chat_interaction.vala
|
||||
src/service/connection_manager.vala
|
||||
src/service/conversation_manager.vala
|
||||
src/service/counterpart_interaction_manager.vala
|
||||
src/service/database.vala
|
||||
src/service/entity_capabilities_storage.vala
|
||||
src/service/message_manager.vala
|
||||
src/service/module_manager.vala
|
||||
src/service/muc_manager.vala
|
||||
src/service/pgp_manager.vala
|
||||
src/service/presence_manager.vala
|
||||
src/service/roster_manager.vala
|
||||
src/service/stream_interactor.vala
|
||||
|
||||
src/settings.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/application.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
|
||||
PACKAGES
|
||||
${CLIENT_PACKAGES}
|
||||
gpgme
|
||||
uuid
|
||||
vala-xmpp
|
||||
qlite
|
||||
GRESOURCES
|
||||
${CLIENT_GRESOURCES_XML}
|
||||
OPTIONS
|
||||
--target-glib=2.38
|
||||
-g
|
||||
--thread
|
||||
--vapidir=${CMAKE_BINARY_DIR}/vala-xmpp
|
||||
--vapidir=${CMAKE_BINARY_DIR}/qlite
|
||||
--vapidir=${CMAKE_SOURCE_DIR}/vapi
|
||||
|
||||
)
|
||||
|
||||
set(CFLAGS ${CLIENT_CFLAGS} ${GPGME_CFLAGS} ${LIBUUID_CFLAGS} -g -I${CMAKE_BINARY_DIR}/vala-xmpp -I${CMAKE_BINARY_DIR}/qlite ${VALA_CFLAGS})
|
||||
add_definitions(${CFLAGS})
|
||||
add_executable(dino ${CLIENT_VALA_C} ${CLIENT_GRESOURCES_TARGET})
|
||||
add_dependencies(dino vala-xmpp-vapi qlite-vapi)
|
||||
target_link_libraries(dino vala-xmpp qlite ${CLIENT_LIBRARIES} ${GPGME_LIBRARIES} ${LIBUUID_LIBRARIES} -lm)
|
||||
|
150
client/data/add_conversation/add_contact_dialog.ui
Normal file
|
@ -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>
|
224
client/data/add_conversation/add_groupchat_dialog.ui
Normal file
|
@ -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>
|
227
client/data/add_conversation/conference_details_fragment.ui
Normal file
|
@ -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>
|
61
client/data/add_conversation/list_row.ui
Normal file
|
@ -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>
|
109
client/data/add_conversation/select_jid_fragment.ui
Normal file
|
@ -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>
|
23
client/data/chat_input.ui
Normal file
|
@ -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>
|
41
client/data/conversation_list_titlebar.ui
Normal file
|
@ -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>
|
23
client/data/conversation_selector/chat_row_tooltip.ui
Normal file
|
@ -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>
|
146
client/data/conversation_selector/conversation_row.ui
Normal file
|
@ -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>
|
33
client/data/conversation_selector/view.ui
Normal file
|
@ -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>
|
98
client/data/conversation_summary/message_item.ui
Normal file
|
@ -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>
|
33
client/data/conversation_summary/view.ui
Normal file
|
@ -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>
|
63
client/data/conversation_titlebar.ui
Normal file
|
@ -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>
|
BIN
client/data/gschemas.compiled
Normal file
190
client/data/img/double_tick.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 6.8 KiB |
1
client/data/img/send.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 344 B |
73
client/data/img/status_away.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 2.3 KiB |
85
client/data/img/status_chat.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 3 KiB |
73
client/data/img/status_dnd.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 2.3 KiB |
67
client/data/img/status_online.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 2 KiB |
184
client/data/img/tick.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 5.9 KiB |
30
client/data/manage_accounts/account_row.ui
Normal file
|
@ -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>
|
137
client/data/manage_accounts/add_account_dialog.ui
Normal file
|
@ -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>
|
306
client/data/manage_accounts/dialog.ui
Normal file
|
@ -0,0 +1,306 @@
|
|||
<?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">
|
||||
<property name="expand">True</property>
|
||||
<property name="column-spacing">10</property>
|
||||
<property name="row-spacing">10</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>
|
||||
</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="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>
|
16
client/data/menu_add.ui
Normal file
|
@ -0,0 +1,16 @@
|
|||
<interface>
|
||||
<menu id="menu_add">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.add_chat</attribute>
|
||||
<attribute name="label" translatable="yes">Start Chat</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.add_conference</attribute>
|
||||
<attribute name="label" translatable="yes">Join Conference</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
20
client/data/menu_app.ui
Normal file
|
@ -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>
|
9
client/data/menu_conversation.ui
Normal file
|
@ -0,0 +1,9 @@
|
|||
<interface>
|
||||
<menu id="menu_conversation">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Contact Details</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
49
client/data/menu_encryption.ui
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?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">
|
||||
<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>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="button_pgp">
|
||||
<property name="label" translatable="yes">OpenPGP</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>
|
||||
<property name="group">button_unencrypted</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="submenu">main</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
43
client/data/occupant_list.ui
Normal file
|
@ -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>
|
44
client/data/occupant_list_item.ui
Normal file
|
@ -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>
|
15
client/data/settings.gschema.xml
Normal file
|
@ -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>
|
51
client/data/settings_dialog.ui
Normal file
|
@ -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>
|
3
client/data/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
scrolledwindow {
|
||||
background-color: white;
|
||||
}
|
178
client/data/unified_window.ui
Normal file
|
@ -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>
|
18
client/src/dbus/login1.vala
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Dino {
|
||||
|
||||
[DBus (name = "org.freedesktop.login1.Manager")]
|
||||
public interface Login1Manager : Object {
|
||||
public signal void PrepareForSleep(bool suspend);
|
||||
}
|
||||
|
||||
public static Login1Manager? get_login1() {
|
||||
Login1Manager? login1 = null;
|
||||
try {
|
||||
login1 = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1");
|
||||
} catch (IOError e) {
|
||||
stderr.printf("%s\n", e.message);
|
||||
}
|
||||
return login1;
|
||||
}
|
||||
|
||||
}
|
22
client/src/dbus/networkmanager.vala
Normal file
|
@ -0,0 +1,22 @@
|
|||
namespace Dino {
|
||||
|
||||
[DBus (name = "org.freedesktop.NetworkManager")]
|
||||
public interface NetworkManager : Object {
|
||||
|
||||
public const int CONNECTED_GLOBAL = 70;
|
||||
|
||||
public abstract uint32 State {owned get;}
|
||||
public signal void StateChanged(uint32 state);
|
||||
}
|
||||
|
||||
public static NetworkManager? get_network_manager() {
|
||||
NetworkManager? nm = null;
|
||||
try {
|
||||
nm = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager");
|
||||
} catch (IOError e) {
|
||||
stderr.printf ("%s\n", e.message);
|
||||
}
|
||||
return nm;
|
||||
}
|
||||
|
||||
}
|
19
client/src/dbus/upower.vala
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace Dino {
|
||||
|
||||
[DBus (name = "org.freedesktop.UPower")]
|
||||
public interface UPower : Object {
|
||||
public signal void Sleeping();
|
||||
public signal void Resuming();
|
||||
}
|
||||
|
||||
public static UPower? get_upower() {
|
||||
UPower? upower = null;
|
||||
try {
|
||||
upower = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower");
|
||||
} catch (IOError e) {
|
||||
stderr.printf ("%s\n", e.message);
|
||||
}
|
||||
return upower;
|
||||
}
|
||||
|
||||
}
|
40
client/src/entity/account.vala
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Gee;
|
||||
|
||||
namespace Dino.Entities {
|
||||
public class Account : Object {
|
||||
|
||||
public int id { get; set; }
|
||||
public string localpart { get { return bare_jid.localpart; } }
|
||||
public string domainpart { get { return bare_jid.domainpart; } }
|
||||
public string resourcepart { get; set; }
|
||||
public Jid bare_jid { get; private set; }
|
||||
public string? password { get; set; }
|
||||
public string display_name {
|
||||
owned get {
|
||||
if (alias != null) {
|
||||
return alias;
|
||||
} else {
|
||||
return bare_jid.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
public string? alias { get; set; }
|
||||
public bool enabled { get; set; }
|
||||
|
||||
public Account.from_bare_jid(string bare_jid) {
|
||||
this.bare_jid = new Jid(bare_jid);
|
||||
}
|
||||
|
||||
public bool equals(Account acc) {
|
||||
return equals_func(this, acc);
|
||||
}
|
||||
|
||||
public static bool equals_func(Account acc1, Account acc2) {
|
||||
return acc1.bare_jid.to_string() == acc2.bare_jid.to_string();
|
||||
}
|
||||
|
||||
public static uint hash_func(Account acc) {
|
||||
return acc.bare_jid.to_string().hash();
|
||||
}
|
||||
}
|
||||
}
|
48
client/src/entity/conversation.vala
Normal file
|
@ -0,0 +1,48 @@
|
|||
namespace Dino.Entities {
|
||||
public class Conversation : Object {
|
||||
|
||||
public signal void object_updated(Conversation conversation);
|
||||
|
||||
public const int ENCRYPTION_UNENCRYPTED = 0;
|
||||
public const int ENCRYPTION_PGP = 1;
|
||||
|
||||
public const int TYPE_CHAT = 0;
|
||||
public const int TYPE_GROUPCHAT = 1;
|
||||
|
||||
public int id { get; set; }
|
||||
public Account account { get; private set; }
|
||||
public Jid counterpart { get; private set; }
|
||||
public bool active { get; set; }
|
||||
public DateTime last_active { get; set; }
|
||||
public int encryption { get; set; }
|
||||
public int? type_ { get; set; }
|
||||
public Message read_up_to { get; set; }
|
||||
|
||||
public Conversation(Jid jid, Account account) {
|
||||
this.counterpart = jid;
|
||||
this.account = account;
|
||||
this.active = false;
|
||||
this.last_active = new DateTime.from_unix_utc(0);
|
||||
this.encryption = ENCRYPTION_UNENCRYPTED;
|
||||
}
|
||||
|
||||
public Conversation.with_id(Jid jid, Account account, int id) {
|
||||
this.counterpart = jid;
|
||||
this.account = account;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public bool equals(Conversation? conversation) {
|
||||
if (conversation == null) return false;
|
||||
return equals_func(this, conversation);
|
||||
}
|
||||
|
||||
public static bool equals_func(Conversation conversation1, Conversation conversation2) {
|
||||
return conversation1.counterpart.equals(conversation2.counterpart) && conversation1.account.equals(conversation2.account);
|
||||
}
|
||||
|
||||
public static uint hash_func(Conversation conversation) {
|
||||
return conversation.counterpart.to_string().hash() ^ conversation.account.bare_jid.to_string().hash();
|
||||
}
|
||||
}
|
||||
}
|
91
client/src/entity/jid.vala
Normal file
|
@ -0,0 +1,91 @@
|
|||
public class Dino.Entities.Jid : Object {
|
||||
public string? localpart { get; set; }
|
||||
public string domainpart { get; set; }
|
||||
public string? resourcepart { get; set; }
|
||||
|
||||
public Jid? bare_jid {
|
||||
owned get { return new Jid(@"$localpart@$domainpart"); }
|
||||
}
|
||||
|
||||
private string jid { get; private set; }
|
||||
|
||||
public Jid(string jid) {
|
||||
Jid? parsed = Jid.parse(jid);
|
||||
string? localpart = parsed != null ? parsed.localpart : null;
|
||||
string domainpart = parsed != null ? parsed.domainpart : jid;
|
||||
string? resourcepart = parsed != null ? parsed.resourcepart : null;
|
||||
Jid.components(localpart, domainpart, resourcepart);
|
||||
}
|
||||
|
||||
public Jid.with_resource(string bare_jid, string resource) {
|
||||
Jid? parsed = Jid.parse(bare_jid);
|
||||
print(parsed.localpart + "\n");
|
||||
print(parsed.domainpart + "\n");
|
||||
Jid.components(parsed.localpart, parsed.domainpart, resourcepart);
|
||||
}
|
||||
|
||||
public Jid.components(string? localpart, string domainpart, string? resourcepart) {
|
||||
string jid = domainpart;
|
||||
if (localpart != null) {
|
||||
jid = @"$localpart@$jid";
|
||||
}
|
||||
if (resourcepart != null) {
|
||||
jid = @"$jid/$resourcepart";
|
||||
}
|
||||
this.jid = jid;
|
||||
this.localpart = localpart;
|
||||
this.domainpart = domainpart;
|
||||
this.resourcepart = resourcepart;
|
||||
}
|
||||
|
||||
public static Jid? parse(string jid) {
|
||||
int slash_index = jid.index_of("/");
|
||||
string resourcepart = slash_index == -1 ? null : jid.slice(slash_index + 1, jid.length);
|
||||
string bare_jid = slash_index == -1 ? jid : jid.slice(0, slash_index);
|
||||
int at_index = bare_jid.index_of("@");
|
||||
string localpart = at_index == -1 ? null : bare_jid.slice(0, at_index);
|
||||
string domainpart = at_index == -1 ? bare_jid : bare_jid.slice(at_index + 1, bare_jid.length);
|
||||
|
||||
if (domainpart == "") return null;
|
||||
if (slash_index != -1 && resourcepart == "") return null;
|
||||
if (at_index != -1 && localpart == "") return null;
|
||||
|
||||
return new Jid.components(localpart, domainpart, resourcepart);
|
||||
}
|
||||
|
||||
public bool is_bare() {
|
||||
return localpart != null && resourcepart == null;
|
||||
}
|
||||
|
||||
public bool is_full() {
|
||||
return localpart != null && resourcepart != null;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return jid;
|
||||
}
|
||||
|
||||
public bool equals_bare(Jid jid) {
|
||||
return equals_bare_func(this, jid);
|
||||
}
|
||||
|
||||
public bool equals(Jid jid) {
|
||||
return equals_func(this, jid);
|
||||
}
|
||||
|
||||
public static new bool equals_bare_func(Jid jid1, Jid jid2) {
|
||||
return jid1.bare_jid.to_string() == jid2.bare_jid.to_string();
|
||||
}
|
||||
|
||||
public static bool equals_func(Jid jid1, Jid jid2) {
|
||||
return jid1.to_string() == jid2.to_string();
|
||||
}
|
||||
|
||||
public static new uint hash_bare_func(Jid jid) {
|
||||
return jid.bare_jid.to_string().hash();
|
||||
}
|
||||
|
||||
public static new uint hash_func(Jid jid) {
|
||||
return jid.to_string().hash();
|
||||
}
|
||||
}
|
89
client/src/entity/message.vala
Normal file
|
@ -0,0 +1,89 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
|
||||
public class Dino.Entities.Message : Object {
|
||||
|
||||
public const bool DIRECTION_SENT = true;
|
||||
public const bool DIRECTION_RECEIVED = false;
|
||||
|
||||
public enum Marked {
|
||||
NONE,
|
||||
RECEIVED,
|
||||
READ,
|
||||
ACKNOWLEDGED
|
||||
}
|
||||
|
||||
public enum Encryption {
|
||||
NONE,
|
||||
PGP
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
ERROR,
|
||||
CHAT,
|
||||
GROUPCHAT,
|
||||
HEADLINE,
|
||||
NORMAL
|
||||
}
|
||||
|
||||
public int? id { get; set; }
|
||||
public Account account { get; set; }
|
||||
public Jid? counterpart { get; set; }
|
||||
public Jid? ourpart { get; set; }
|
||||
public Jid? from {
|
||||
get { return direction == DIRECTION_SENT ? account.bare_jid : counterpart; }
|
||||
}
|
||||
public Jid? to {
|
||||
get { return direction == DIRECTION_SENT ? counterpart : account.bare_jid; }
|
||||
}
|
||||
public bool direction { get; set; }
|
||||
public string? real_jid { get; set; }
|
||||
public Type type_ { get; set; }
|
||||
public string? body { get; set; }
|
||||
public string? stanza_id { get; set; }
|
||||
public DateTime? time { get; set; }
|
||||
public DateTime? local_time { get; set; }
|
||||
public Encryption encryption { get; set; default = Encryption.NONE; }
|
||||
public Marked marked { get; set; default = Marked.NONE; }
|
||||
public Xmpp.Message.Stanza stanza { get; set; }
|
||||
|
||||
public void set_type_string(string type) {
|
||||
switch (type) {
|
||||
case Xmpp.Message.Stanza.TYPE_CHAT:
|
||||
type_ = Type.CHAT; break;
|
||||
case Xmpp.Message.Stanza.TYPE_GROUPCHAT:
|
||||
type_ = Type.GROUPCHAT; break;
|
||||
default:
|
||||
type_ = Type.NORMAL; break;
|
||||
}
|
||||
}
|
||||
|
||||
public new string get_type_string() {
|
||||
switch (type_) {
|
||||
case Type.CHAT:
|
||||
return Xmpp.Message.Stanza.TYPE_CHAT;
|
||||
case Type.GROUPCHAT:
|
||||
return Xmpp.Message.Stanza.TYPE_GROUPCHAT;
|
||||
default:
|
||||
return Xmpp.Message.Stanza.TYPE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
public bool equals(Message? m) {
|
||||
if (m == null) return false;
|
||||
return equals_func(this, m);
|
||||
}
|
||||
|
||||
public static bool equals_func(Message m1, Message m2) {
|
||||
if (m1.stanza_id == m2.stanza_id &&
|
||||
m1.body == m2.body) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static uint hash_func(Message message) {
|
||||
return message.body.hash();
|
||||
}
|
||||
}
|
12
client/src/main.vala
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Dino.Entities;
|
||||
using Dino.Ui;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
void main(string[] args) {
|
||||
Notify.init("dino");
|
||||
Gtk.init(ref args);
|
||||
Dino.Ui.Application app = new Dino.Ui.Application();
|
||||
app.run(args);
|
||||
}
|
||||
}
|
134
client/src/service/avatar_manager.vala
Normal file
|
@ -0,0 +1,134 @@
|
|||
using Gdk;
|
||||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class AvatarManager : StreamInteractionModule, Object {
|
||||
public const string id = "avatar_manager";
|
||||
|
||||
public signal void received_avatar(Pixbuf avatar, Jid jid, Account account);
|
||||
|
||||
private enum Source {
|
||||
USER_AVATARS,
|
||||
VCARD
|
||||
}
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Database db;
|
||||
private HashMap<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
|
||||
private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
|
||||
private AvatarStorage avatar_storage = new AvatarStorage("./"); // TODO ihh
|
||||
private const int MAX_PIXEL = 192;
|
||||
|
||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||
AvatarManager m = new AvatarManager(stream_interactor, db);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private AvatarManager(StreamInteractor stream_interactor, Database db) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
this.db = db;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
}
|
||||
|
||||
public Pixbuf? get_avatar(Account account, Jid jid) {
|
||||
Jid jid_ = jid;
|
||||
if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
|
||||
jid_ = jid.bare_jid;
|
||||
}
|
||||
string? user_avatars_id = user_avatars[jid_];
|
||||
if (user_avatars_id != null) {
|
||||
return avatar_storage.get_image(user_avatars_id);
|
||||
}
|
||||
string? vcard_avatars_id = vcard_avatars[jid_];
|
||||
if (vcard_avatars_id != null) {
|
||||
return avatar_storage.get_image(vcard_avatars_id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void publish(Account account, string file) {
|
||||
print(file + "\n");
|
||||
try {
|
||||
Pixbuf pixbuf = new Pixbuf.from_file(file);
|
||||
if (pixbuf.width >= pixbuf.height && pixbuf.width > MAX_PIXEL) {
|
||||
int dest_height = (int) ((float) MAX_PIXEL / pixbuf.width * pixbuf.height);
|
||||
pixbuf = pixbuf.scale_simple(MAX_PIXEL, dest_height, InterpType.BILINEAR);
|
||||
} else if (pixbuf.height > pixbuf.width && pixbuf.width > MAX_PIXEL) {
|
||||
int dest_width = (int) ((float) MAX_PIXEL / pixbuf.height * pixbuf.width);
|
||||
pixbuf = pixbuf.scale_simple(dest_width, MAX_PIXEL, InterpType.BILINEAR);
|
||||
}
|
||||
uint8[] buffer;
|
||||
pixbuf.save_to_buffer(out buffer, "png");
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xep.UserAvatars.Module.get_module(stream).publish_png(stream, buffer, pixbuf.width, pixbuf.height);
|
||||
on_user_avatar_received(account, account.bare_jid, Base64.encode(buffer));
|
||||
}
|
||||
} catch (Error e) {
|
||||
print("error " + e.message + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
private class PublishResponseListenerImpl : Object {
|
||||
public void on_success(Core.XmppStream stream) {
|
||||
|
||||
}
|
||||
public void on_error(Core.XmppStream stream) { }
|
||||
}
|
||||
|
||||
public static AvatarManager? get_instance(StreamInteractor stream_interaction) {
|
||||
return (AvatarManager) stream_interaction.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.user_avatars_modules[account].received_avatar.connect((stream, jid, id) =>
|
||||
on_user_avatar_received(account, new Jid(jid), id)
|
||||
);
|
||||
stream_interactor.module_manager.vcard_modules[account].received_avatar.connect((stream, jid, id) =>
|
||||
on_vcard_avatar_received(account, new Jid(jid), id)
|
||||
);
|
||||
|
||||
user_avatars = db.get_avatar_hashes(Source.USER_AVATARS);
|
||||
foreach (Jid jid in user_avatars.keys) {
|
||||
on_user_avatar_received(account, jid, user_avatars[jid]);
|
||||
}
|
||||
vcard_avatars = db.get_avatar_hashes(Source.VCARD);
|
||||
foreach (Jid jid in vcard_avatars.keys) {
|
||||
on_vcard_avatar_received(account, jid, vcard_avatars[jid]);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_user_avatar_received(Account account, Jid jid, string id) {
|
||||
if (!user_avatars.has_key(jid) || user_avatars[jid] != id) {
|
||||
user_avatars[jid] = id;
|
||||
db.set_avatar_hash(jid, id, Source.USER_AVATARS);
|
||||
}
|
||||
Pixbuf? avatar = avatar_storage.get_image(id);
|
||||
if (avatar != null) {
|
||||
received_avatar(avatar, jid, account);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_vcard_avatar_received(Account account, Jid jid, string id) {
|
||||
if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) {
|
||||
vcard_avatars[jid] = id;
|
||||
if (!jid.is_full()) { // don't save muc avatars
|
||||
db.set_avatar_hash(jid, id, Source.VCARD);
|
||||
}
|
||||
}
|
||||
Pixbuf? avatar = avatar_storage.get_image(id);
|
||||
if (avatar != null) {
|
||||
received_avatar(avatar, jid, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
34
client/src/service/avatar_storage.vala
Normal file
|
@ -0,0 +1,34 @@
|
|||
using Gdk;
|
||||
|
||||
using Xmpp;
|
||||
|
||||
namespace Dino {
|
||||
public class AvatarStorage : Xep.PixbufStorage, Object {
|
||||
|
||||
string folder;
|
||||
|
||||
public AvatarStorage(string folder) {
|
||||
this.folder = folder;
|
||||
}
|
||||
|
||||
public void store(string id, uint8[] data) {
|
||||
File file = File.new_for_path(id);
|
||||
if (file.query_exists()) file.delete(); //TODO y?
|
||||
DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));
|
||||
fos.write(data);
|
||||
}
|
||||
|
||||
public bool has_image(string id) {
|
||||
File file = File.new_for_path(folder + id);
|
||||
return file.query_exists();
|
||||
}
|
||||
|
||||
public Pixbuf? get_image(string id) {
|
||||
try {
|
||||
return new Pixbuf.from_file(folder + id);
|
||||
} catch (Error e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
client/src/service/chat_interaction.vala
Normal file
|
@ -0,0 +1,146 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class ChatInteraction : StreamInteractionModule, Object {
|
||||
private const string id = "chat_interaction";
|
||||
|
||||
public signal void conversation_read(Conversation conversation);
|
||||
public signal void conversation_unread(Conversation conversation);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Conversation? selected_conversation;
|
||||
|
||||
private HashMap<Conversation, DateTime> last_input_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
|
||||
private HashMap<Conversation, DateTime> last_interface_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
|
||||
private bool focus_in = false;
|
||||
|
||||
public static void start(StreamInteractor stream_interactor) {
|
||||
ChatInteraction m = new ChatInteraction(stream_interactor);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private ChatInteraction(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
Timeout.add_seconds(30, update_interactions);
|
||||
MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received);
|
||||
MessageManager.get_instance(stream_interactor).message_sent.connect(on_message_sent);
|
||||
}
|
||||
|
||||
public bool is_active_focus(Conversation? conversation = null) {
|
||||
if (conversation != null) {
|
||||
return focus_in && conversation.equals(this.selected_conversation);
|
||||
} else {
|
||||
return focus_in;
|
||||
}
|
||||
}
|
||||
|
||||
public void window_focus_in(Conversation? conversation) {
|
||||
on_conversation_selected(selected_conversation);
|
||||
}
|
||||
|
||||
public void window_focus_out(Conversation? conversation) {
|
||||
focus_in = false;
|
||||
}
|
||||
|
||||
public void on_message_entered(Conversation conversation) {
|
||||
if (Settings.instance().send_read) {
|
||||
if (!last_input_interaction.has_key(conversation) && conversation.type_ != Conversation.TYPE_GROUPCHAT) {
|
||||
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING);
|
||||
}
|
||||
}
|
||||
last_input_interaction[conversation] = new DateTime.now_utc();
|
||||
last_interface_interaction[conversation] = new DateTime.now_utc();
|
||||
}
|
||||
|
||||
public void on_message_cleared(Conversation conversation) {
|
||||
if (last_input_interaction.has_key(conversation)) {
|
||||
last_input_interaction.unset(conversation);
|
||||
last_interface_interaction.unset(conversation);
|
||||
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
public void on_conversation_selected(Conversation? conversation) {
|
||||
selected_conversation = conversation;
|
||||
focus_in = true;
|
||||
if (conversation != null) {
|
||||
conversation_read(selected_conversation);
|
||||
check_send_read();
|
||||
selected_conversation.read_up_to = MessageManager.get_instance(stream_interactor).get_last_message(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static ChatInteraction? get_instance(StreamInteractor stream_interactor) {
|
||||
return (ChatInteraction) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
private void check_send_read() {
|
||||
if (selected_conversation == null || selected_conversation.type_ == Conversation.TYPE_GROUPCHAT) return;
|
||||
Entities.Message? message = MessageManager.get_instance(stream_interactor).get_last_message(selected_conversation);
|
||||
if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED &&
|
||||
message.stanza != null && !message.equals(selected_conversation.read_up_to)) {
|
||||
selected_conversation.read_up_to = message;
|
||||
send_chat_marker(selected_conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED);
|
||||
}
|
||||
}
|
||||
|
||||
private bool update_interactions() {
|
||||
ArrayList<Conversation> remove_input = new ArrayList<Conversation>(Conversation.equals_func);
|
||||
ArrayList<Conversation> remove_interface = new ArrayList<Conversation>(Conversation.equals_func);
|
||||
foreach (Conversation conversation in last_input_interaction.keys) {
|
||||
if (last_input_interaction.has_key(conversation) &&
|
||||
(new DateTime.now_utc()).difference(last_input_interaction[conversation]) >= 15 * TimeSpan.SECOND) {
|
||||
remove_input.add(conversation);
|
||||
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED);
|
||||
}
|
||||
}
|
||||
foreach (Conversation conversation in last_interface_interaction.keys) {
|
||||
if (last_interface_interaction.has_key(conversation) &&
|
||||
(new DateTime.now_utc()).difference(last_interface_interaction[conversation]) >= 1.5 * TimeSpan.MINUTE) {
|
||||
remove_interface.add(conversation);
|
||||
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_GONE);
|
||||
}
|
||||
}
|
||||
foreach (Conversation conversation in remove_input) last_input_interaction.unset(conversation);
|
||||
foreach (Conversation conversation in remove_interface) last_interface_interaction.unset(conversation);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void on_message_received(Entities.Message message, Conversation conversation) {
|
||||
if (is_active_focus(conversation)) {
|
||||
check_send_read();
|
||||
conversation.read_up_to = message;
|
||||
send_chat_marker(conversation, message, Xep.ChatMarkers.MARKER_DISPLAYED);
|
||||
} else {
|
||||
conversation_unread(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_message_sent(Entities.Message message, Conversation conversation) {
|
||||
last_input_interaction.unset(conversation);
|
||||
last_interface_interaction.unset(conversation);
|
||||
conversation.read_up_to = message;
|
||||
}
|
||||
|
||||
private void send_chat_marker(Conversation conversation, Entities.Message message, string marker) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
|
||||
if (stream != null && Settings.instance().send_read && Xep.ChatMarkers.Module.requests_marking(message.stanza)) {
|
||||
Xep.ChatMarkers.Module.get_module(stream).send_marker(stream, message.stanza.from, message.stanza_id, message.get_type_string(), marker);
|
||||
}
|
||||
}
|
||||
|
||||
private void send_chat_state_notification(Conversation conversation, string state) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
|
||||
if (stream != null && Settings.instance().send_read) {
|
||||
Xep.ChatStateNotifications.Module.get_module(stream).send_state(stream, conversation.counterpart.to_string(), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
222
client/src/service/connection_manager.vala
Normal file
|
@ -0,0 +1,222 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class ConnectionManager {
|
||||
|
||||
public signal void stream_opened(Account account, Core.XmppStream stream);
|
||||
public signal void connection_state_changed(Account account, ConnectionState state);
|
||||
|
||||
public enum ConnectionState {
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED
|
||||
}
|
||||
|
||||
private ArrayList<Account> connection_todo = new ArrayList<Account>(Account.equals_func);
|
||||
private HashMap<Account, Connection> stream_states = new HashMap<Account, Connection>(Account.hash_func, Account.equals_func);
|
||||
private NetworkManager? network_manager;
|
||||
private Login1Manager? login1;
|
||||
private ModuleManager module_manager;
|
||||
|
||||
private class Connection {
|
||||
public Core.XmppStream stream { get; set; }
|
||||
public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; }
|
||||
public DateTime established { get; set; }
|
||||
public class Connection(Core.XmppStream stream, DateTime established) {
|
||||
this.stream = stream;
|
||||
this.established = established;
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionManager(ModuleManager module_manager) {
|
||||
this.module_manager = module_manager;
|
||||
network_manager = get_network_manager();
|
||||
if (network_manager != null) {
|
||||
network_manager.StateChanged.connect(on_nm_state_changed);
|
||||
}
|
||||
login1 = get_login1();
|
||||
if (login1 != null) {
|
||||
login1.PrepareForSleep.connect(on_prepare_for_sleep);
|
||||
}
|
||||
}
|
||||
|
||||
public Core.XmppStream? get_stream(Account account) {
|
||||
if (get_connection_state(account) == ConnectionState.CONNECTED) {
|
||||
return stream_states[account].stream;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ConnectionState get_connection_state(Account account) {
|
||||
if (stream_states.has_key(account)){
|
||||
return stream_states[account].connection_state;
|
||||
}
|
||||
return ConnectionState.DISCONNECTED;
|
||||
}
|
||||
|
||||
public ArrayList<Account> get_managed_accounts() {
|
||||
return connection_todo;
|
||||
}
|
||||
|
||||
public Core.XmppStream? connect(Account account) {
|
||||
if (!connection_todo.contains(account)) connection_todo.add(account);
|
||||
if (!stream_states.has_key(account)) {
|
||||
return connect_(account);
|
||||
} else {
|
||||
check_reconnect(account);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void disconnect(Account account) {
|
||||
change_connection_state(account, ConnectionState.DISCONNECTED);
|
||||
if (stream_states.has_key(account)) {
|
||||
try {
|
||||
stream_states[account].stream.disconnect();
|
||||
} catch (Error e) { }
|
||||
}
|
||||
connection_todo.remove(account);
|
||||
}
|
||||
|
||||
private Core.XmppStream? connect_(Account account, string? resource = null) {
|
||||
if (resource == null) resource = account.resourcepart;
|
||||
if (stream_states.has_key(account)) {
|
||||
stream_states[account].stream.remove_modules();
|
||||
}
|
||||
|
||||
Core.XmppStream stream = new Core.XmppStream();
|
||||
foreach (Core.XmppStreamModule module in module_manager.get_modules(account, resource)) {
|
||||
stream.add_module(module);
|
||||
}
|
||||
stream.debug = true;
|
||||
|
||||
Connection connection = new Connection(stream, new DateTime.now_local());
|
||||
stream_states[account] = connection;
|
||||
change_connection_state(account, ConnectionState.CONNECTING);
|
||||
stream.stream_negotiated.connect((stream) => {
|
||||
change_connection_state(account, ConnectionState.CONNECTED);
|
||||
});
|
||||
new Thread<void*> (null, () => {
|
||||
try {
|
||||
stream.connect(account.domainpart);
|
||||
} catch (Error e) {
|
||||
stderr.printf("Stream Error: %s\n", e.message);
|
||||
change_connection_state(account, ConnectionState.DISCONNECTED);
|
||||
interpret_reconnect_flags(account, StreamError.Flag.get_flag(stream) ??
|
||||
new StreamError.Flag() { reconnection_recomendation = StreamError.Flag.Reconnect.NOW });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
stream_opened(account, stream);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private void interpret_reconnect_flags(Account account, StreamError.Flag stream_error_flag) {
|
||||
if (!connection_todo.contains(account)) return;
|
||||
int wait_sec = 10;
|
||||
if (network_manager != null && network_manager.State != NetworkManager.CONNECTED_GLOBAL) {
|
||||
wait_sec = 60;
|
||||
}
|
||||
switch (stream_error_flag.reconnection_recomendation) {
|
||||
case StreamError.Flag.Reconnect.NOW:
|
||||
wait_sec = 10;
|
||||
break;
|
||||
case StreamError.Flag.Reconnect.LATER:
|
||||
case StreamError.Flag.Reconnect.UNKNOWN:
|
||||
wait_sec = 60;
|
||||
break;
|
||||
case StreamError.Flag.Reconnect.NEVER:
|
||||
return;
|
||||
}
|
||||
print(@"recovering in $wait_sec\n");
|
||||
Timeout.add_seconds(wait_sec, () => {
|
||||
if (stream_error_flag.resource_rejected) {
|
||||
connect_(account, account.resourcepart + "-" + UUID.generate_random_unparsed());
|
||||
} else {
|
||||
connect_(account);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void check_reconnects() {
|
||||
foreach (Account account in connection_todo) {
|
||||
check_reconnect(account);
|
||||
}
|
||||
}
|
||||
|
||||
private void check_reconnect(Account account) {
|
||||
PingResponseListenerImpl ping_response_listener = new PingResponseListenerImpl(this, account);
|
||||
Core.XmppStream stream = stream_states[account].stream;
|
||||
Xep.Ping.Module.get_module(stream).send_ping(stream, account.domainpart, ping_response_listener);
|
||||
|
||||
Timeout.add_seconds(5, () => {
|
||||
if (stream_states[account].stream != stream) return false;
|
||||
if (ping_response_listener.acked) return false;
|
||||
|
||||
change_connection_state(account, ConnectionState.DISCONNECTED);
|
||||
try {
|
||||
stream_states[account].stream.disconnect();
|
||||
} catch (Error e) { }
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private class PingResponseListenerImpl : Xep.Ping.ResponseListener, Object {
|
||||
public bool acked = false;
|
||||
ConnectionManager outer;
|
||||
Account account;
|
||||
public PingResponseListenerImpl(ConnectionManager outer, Account account) {
|
||||
this.outer = outer;
|
||||
this.account = account;
|
||||
}
|
||||
public void on_result(Core.XmppStream stream) {
|
||||
print("ping ok\n");
|
||||
acked = true;
|
||||
outer.change_connection_state(account, ConnectionState.CONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_nm_state_changed(uint32 state) {
|
||||
print("nm " + state.to_string() + "\n");
|
||||
if (state == NetworkManager.CONNECTED_GLOBAL) {
|
||||
check_reconnects();
|
||||
} else {
|
||||
foreach (Account account in connection_todo) {
|
||||
change_connection_state(account, ConnectionState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_prepare_for_sleep(bool suspend) {
|
||||
foreach (Account account in connection_todo) {
|
||||
change_connection_state(account, ConnectionState.DISCONNECTED);
|
||||
}
|
||||
if (suspend) {
|
||||
print("suspend\n");
|
||||
foreach (Account account in connection_todo) {
|
||||
Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza();
|
||||
presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE;
|
||||
try {
|
||||
Presence.Module.get_module(stream_states[account].stream).send_presence(stream_states[account].stream, presence);
|
||||
stream_states[account].stream.disconnect();
|
||||
} catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); }
|
||||
}
|
||||
} else {
|
||||
print("un-suspend\n");
|
||||
check_reconnects();
|
||||
}
|
||||
}
|
||||
|
||||
private void change_connection_state(Account account, ConnectionState state) {
|
||||
stream_states[account].connection_state = state;
|
||||
connection_state_changed(account, state);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
98
client/src/service/conversation_manager.vala
Normal file
|
@ -0,0 +1,98 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class ConversationManager : StreamInteractionModule, Object {
|
||||
|
||||
public const string id = "conversation_manager";
|
||||
|
||||
public signal void conversation_activated(Conversation conversation);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Database db;
|
||||
|
||||
private HashMap<Account, HashMap<Jid, Conversation>> conversations = new HashMap<Account, HashMap<Jid, Conversation>>(Account.hash_func, Account.equals_func);
|
||||
|
||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||
ConversationManager m = new ConversationManager(stream_interactor, db);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private ConversationManager(StreamInteractor stream_interactor, Database db) {
|
||||
this.db = db;
|
||||
this.stream_interactor = stream_interactor;
|
||||
stream_interactor.add_module(this);
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
MucManager.get_instance(stream_interactor).groupchat_joined.connect(on_groupchat_joined);
|
||||
MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_message_received);
|
||||
}
|
||||
|
||||
public Conversation? get_conversation(Jid jid, Account account) {
|
||||
if (conversations.has_key(account)) {
|
||||
return conversations[account][jid];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Conversation get_add_conversation(Jid jid, Account account) {
|
||||
ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
|
||||
return get_conversation(jid, account);
|
||||
}
|
||||
|
||||
public void ensure_start_conversation(Jid jid, Account account) {
|
||||
ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
|
||||
Conversation? conversation = get_conversation(jid, account);
|
||||
if (conversation != null) {
|
||||
conversation.last_active = new DateTime.now_utc();
|
||||
if (!conversation.active) {
|
||||
conversation.active = true;
|
||||
conversation_activated(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static ConversationManager? get_instance(StreamInteractor stream_interaction) {
|
||||
return (ConversationManager) stream_interaction.get_module(id);
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
conversations[account] = new HashMap<Jid, Conversation>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
foreach (Conversation conversation in db.get_conversations(account)) {
|
||||
add_conversation(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_message_received(Entities.Message message, Conversation conversation) {
|
||||
ensure_start_conversation(conversation.counterpart, conversation.account);
|
||||
}
|
||||
|
||||
private void on_groupchat_joined(Account account, Jid jid, string nick) {
|
||||
ensure_add_conversation(jid, account, Conversation.TYPE_GROUPCHAT);
|
||||
ensure_start_conversation(jid, account);
|
||||
}
|
||||
|
||||
private void ensure_add_conversation(Jid jid, Account account, int type) {
|
||||
if (conversations.has_key(account) && !conversations[account].has_key(jid)) {
|
||||
Conversation conversation = new Conversation(jid, account);
|
||||
conversation.type_ = type;
|
||||
add_conversation(conversation);
|
||||
db.add_conversation(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private void add_conversation(Conversation conversation) {
|
||||
conversations[conversation.account][conversation.counterpart] = conversation;
|
||||
if (conversation.active) {
|
||||
conversation_activated(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
99
client/src/service/counterpart_interaction_manager.vala
Normal file
|
@ -0,0 +1,99 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class CounterpartInteractionManager : StreamInteractionModule, Object {
|
||||
public const string id = "counterpart_interaction_manager";
|
||||
|
||||
public signal void received_state(Account account, Jid jid, string state);
|
||||
public signal void received_marker(Account account, Jid jid, Entities.Message message, string marker);
|
||||
public signal void received_message_received(Account account, Jid jid, Entities.Message message);
|
||||
public signal void received_message_displayed(Account account, Jid jid, Entities.Message message);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private HashMap<Jid, Entities.Message> last_read = new HashMap<Jid, Entities.Message>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
private HashMap<Jid, string> chat_states = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
|
||||
public static void start(StreamInteractor stream_interactor) {
|
||||
CounterpartInteractionManager m = new CounterpartInteractionManager(stream_interactor);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private CounterpartInteractionManager(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received);
|
||||
}
|
||||
|
||||
public string? get_chat_state(Account account, Jid jid) {
|
||||
return chat_states[jid];
|
||||
}
|
||||
|
||||
public Entities.Message? get_last_read(Account account, Jid jid) {
|
||||
return last_read[jid];
|
||||
}
|
||||
|
||||
public static CounterpartInteractionManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (CounterpartInteractionManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.chat_markers_modules[account].marker_received.connect( (stream, jid, marker, id) => {
|
||||
on_chat_marker_received(account, new Jid(jid), marker, id);
|
||||
});
|
||||
stream_interactor.module_manager.message_delivery_receipts_modules[account].receipt_received.connect((stream, jid, id) => {
|
||||
on_receipt_received(account, new Jid(jid), id);
|
||||
});
|
||||
stream_interactor.module_manager.chat_state_notifications_modules[account].chat_state_received.connect((stream, jid, state) => {
|
||||
on_chat_state_received(account, new Jid(jid), state);
|
||||
});
|
||||
}
|
||||
|
||||
private void on_chat_state_received(Account account, Jid jid, string state) {
|
||||
chat_states[jid] = state;
|
||||
received_state(account, jid, state);
|
||||
}
|
||||
|
||||
private void on_chat_marker_received(Account account, Jid jid, string marker, string stanza_id) {
|
||||
Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
|
||||
if (conversation != null) {
|
||||
Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
|
||||
if (messages != null) { // TODO not here
|
||||
foreach (Entities.Message message in messages) {
|
||||
if (message.stanza_id == stanza_id) {
|
||||
switch (marker) {
|
||||
case Xep.ChatMarkers.MARKER_RECEIVED:
|
||||
received_message_received(account, jid, message);
|
||||
message.marked = Entities.Message.Marked.RECEIVED;
|
||||
break;
|
||||
case Xep.ChatMarkers.MARKER_DISPLAYED:
|
||||
last_read[jid] = message;
|
||||
received_message_displayed(account, jid, message);
|
||||
foreach (Entities.Message m in messages) {
|
||||
if (m.equals(message)) break;
|
||||
if (m.marked == Entities.Message.Marked.RECEIVED) m.marked = Entities.Message.Marked.READ;
|
||||
}
|
||||
message.marked = Entities.Message.Marked.READ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_message_received(Entities.Message message, Conversation conversation) {
|
||||
on_chat_state_received(conversation.account, conversation.counterpart, Xep.ChatStateNotifications.STATE_ACTIVE);
|
||||
}
|
||||
|
||||
private void on_receipt_received(Account account, Jid jid, string id) {
|
||||
on_chat_marker_received(account, jid, Xep.ChatMarkers.MARKER_RECEIVED, id);
|
||||
}
|
||||
}
|
||||
}
|
457
client/src/service/database.vala
Normal file
|
@ -0,0 +1,457 @@
|
|||
using Gee;
|
||||
using Sqlite;
|
||||
using Qlite;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class Database : Qlite.Database {
|
||||
private const int VERSION = 0;
|
||||
|
||||
public class AccountTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<string> bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
|
||||
public Column<string> resourcepart = new Column.Text("resourcepart");
|
||||
public Column<string> password = new Column.Text("password");
|
||||
public Column<string> alias = new Column.Text("alias");
|
||||
public Column<bool> enabled = new Column.BoolInt("enabled");
|
||||
|
||||
protected AccountTable(Database db) {
|
||||
base(db, "account");
|
||||
init({id, bare_jid, resourcepart, password, alias, enabled});
|
||||
}
|
||||
}
|
||||
|
||||
public class JidTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<string> bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
|
||||
|
||||
protected JidTable(Database db) {
|
||||
base(db, "jid");
|
||||
init({id, bare_jid});
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<string> stanza_id = new Column.Text("stanza_id");
|
||||
public Column<int> account_id = new Column.Integer("account_id");
|
||||
public Column<int> counterpart_id = new Column.Integer("counterpart_id");
|
||||
public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
|
||||
public Column<string> our_resource = new Column.Text("our_resource");
|
||||
public Column<bool> direction = new Column.BoolInt("direction");
|
||||
public Column<int> type_ = new Column.Integer("type");
|
||||
public Column<long> time = new Column.Long("time");
|
||||
public Column<long> local_time = new Column.Long("local_time");
|
||||
public Column<string> body = new Column.Text("body");
|
||||
public Column<int> encryption = new Column.Integer("encryption");
|
||||
public Column<int> marked = new Column.Integer("marked");
|
||||
|
||||
protected MessageTable(Database db) {
|
||||
base(db, "message");
|
||||
init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
|
||||
type_, time, local_time, body, encryption, marked});
|
||||
}
|
||||
}
|
||||
|
||||
public class RealJidTable : Table {
|
||||
public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
|
||||
public Column<string> real_jid = new Column.Text("real_jid");
|
||||
|
||||
protected RealJidTable(Database db) {
|
||||
base(db, "real_jid");
|
||||
init({message_id, real_jid});
|
||||
}
|
||||
}
|
||||
|
||||
public class UndecryptedTable : Table {
|
||||
public Column<int> message_id = new Column.Integer("message_id");
|
||||
public Column<int> type_ = new Column.Integer("type");
|
||||
public Column<string> data = new Column.Text("data");
|
||||
|
||||
protected UndecryptedTable(Database db) {
|
||||
base(db, "undecrypted");
|
||||
init({message_id, type_, data});
|
||||
}
|
||||
}
|
||||
|
||||
public class ConversationTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||
public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
|
||||
public Column<bool> active = new Column.BoolInt("active");
|
||||
public Column<long> last_active = new Column.Long("last_active");
|
||||
public Column<int> type_ = new Column.Integer("type");
|
||||
public Column<int> encryption = new Column.Integer("encryption");
|
||||
public Column<int> read_up_to = new Column.Integer("read_up_to");
|
||||
|
||||
protected ConversationTable(Database db) {
|
||||
base(db, "conversation");
|
||||
init({id, account_id, jid_id, active, last_active, type_, encryption, read_up_to});
|
||||
}
|
||||
}
|
||||
|
||||
public class AvatarTable : Table {
|
||||
public Column<string> jid = new Column.Text("jid");
|
||||
public Column<string> hash = new Column.Text("hash");
|
||||
public Column<int> type_ = new Column.Integer("type");
|
||||
|
||||
protected AvatarTable(Database db) {
|
||||
base(db, "avatar");
|
||||
init({jid, hash, type_});
|
||||
}
|
||||
}
|
||||
|
||||
public class PgpTable : Table {
|
||||
public Column<string> jid = new Column.Text("jid") { primary_key = true };
|
||||
public Column<string> key = new Column.Text("key") { not_null = true };
|
||||
|
||||
protected PgpTable(Database db) {
|
||||
base(db, "pgp");
|
||||
init({jid, key});
|
||||
}
|
||||
}
|
||||
|
||||
public class EntityFeatureTable : Table {
|
||||
public Column<string> entity = new Column.Text("entity");
|
||||
public Column<string> feature = new Column.Text("feature");
|
||||
|
||||
protected EntityFeatureTable(Database db) {
|
||||
base(db, "entity_feature");
|
||||
init({entity, feature});
|
||||
}
|
||||
}
|
||||
|
||||
public AccountTable account { get; private set; }
|
||||
public JidTable jid { get; private set; }
|
||||
public MessageTable message { get; private set; }
|
||||
public RealJidTable real_jid { get; private set; }
|
||||
public ConversationTable conversation { get; private set; }
|
||||
public AvatarTable avatar { get; private set; }
|
||||
public PgpTable pgp { get; private set; }
|
||||
public EntityFeatureTable entity_feature { get; private set; }
|
||||
|
||||
public Database(string fileName) {
|
||||
base(fileName, VERSION);
|
||||
account = new AccountTable(this);
|
||||
jid = new JidTable(this);
|
||||
message = new MessageTable(this);
|
||||
real_jid = new RealJidTable(this);
|
||||
conversation = new ConversationTable(this);
|
||||
avatar = new AvatarTable(this);
|
||||
pgp = new PgpTable(this);
|
||||
entity_feature = new EntityFeatureTable(this);
|
||||
init({ account, jid, message, real_jid, conversation, avatar, pgp, entity_feature });
|
||||
}
|
||||
|
||||
public override void migrate(long oldVersion) {
|
||||
// new table columns are added, outdated columns are still present
|
||||
}
|
||||
|
||||
public void add_account(Account new_account) {
|
||||
new_account.id = (int) account.insert()
|
||||
.value(account.bare_jid, new_account.bare_jid.to_string())
|
||||
.value(account.resourcepart, new_account.resourcepart)
|
||||
.value(account.password, new_account.password)
|
||||
.value(account.alias, new_account.alias)
|
||||
.value(account.enabled, new_account.enabled)
|
||||
.perform();
|
||||
new_account.notify.connect(on_account_update);
|
||||
}
|
||||
|
||||
private void on_account_update(Object o, ParamSpec sp) {
|
||||
Account changed_account = (Account) o;
|
||||
account.update().with(account.id, "=", changed_account.id)
|
||||
.set(account.bare_jid, changed_account.bare_jid.to_string())
|
||||
.set(account.resourcepart, changed_account.resourcepart)
|
||||
.set(account.password, changed_account.password)
|
||||
.set(account.alias, changed_account.alias)
|
||||
.set(account.enabled, changed_account.enabled)
|
||||
.perform();
|
||||
}
|
||||
|
||||
public void remove_account(Account to_delete) {
|
||||
account.delete().with(account.bare_jid, "=", to_delete.bare_jid.to_string()).perform();
|
||||
}
|
||||
|
||||
public ArrayList<Account> get_accounts() {
|
||||
ArrayList<Account> ret = new ArrayList<Account>();
|
||||
foreach(Row row in account.select()) {
|
||||
Account account = get_account_from_row(row);
|
||||
account.notify.connect(on_account_update);
|
||||
ret.add(account);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Account? get_account_by_id(int id) {
|
||||
Row? row = account.row_with(account.id, id);
|
||||
if (row != null) {
|
||||
return get_account_from_row(row);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Account get_account_from_row(Row row) {
|
||||
Account new_account = new Account.from_bare_jid(row[account.bare_jid]);
|
||||
|
||||
new_account.id = row[account.id];
|
||||
new_account.resourcepart = row[account.resourcepart];
|
||||
new_account.password = row[account.password];
|
||||
new_account.alias = row[account.alias];
|
||||
new_account.enabled = row[account.enabled];
|
||||
return new_account;
|
||||
}
|
||||
|
||||
public void add_message(Message new_message, Account account) {
|
||||
if (new_message.body == null || new_message.stanza_id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
new_message.id = (int) message.insert()
|
||||
.value(message.stanza_id, new_message.stanza_id)
|
||||
.value(message.account_id, new_message.account.id)
|
||||
.value(message.counterpart_id, get_jid_id(new_message.counterpart))
|
||||
.value(message.counterpart_resource, new_message.counterpart.resourcepart)
|
||||
.value(message.our_resource, new_message.ourpart.resourcepart)
|
||||
.value(message.direction, new_message.direction)
|
||||
.value(message.type_, new_message.type_)
|
||||
.value(message.time, (long) new_message.time.to_unix())
|
||||
.value(message.local_time, (long) new_message.local_time.to_unix())
|
||||
.value(message.body, new_message.body)
|
||||
.value(message.encryption, new_message.encryption)
|
||||
.value(message.marked, new_message.marked)
|
||||
.perform();
|
||||
|
||||
if (new_message.real_jid != null) {
|
||||
real_jid.insert()
|
||||
.value(real_jid.message_id, new_message.id)
|
||||
.value(real_jid.real_jid, new_message.real_jid)
|
||||
.perform();
|
||||
}
|
||||
new_message.notify.connect(on_message_update);
|
||||
}
|
||||
|
||||
private void on_message_update(Object o, ParamSpec sp) {
|
||||
Message changed_message = (Message) o;
|
||||
UpdateBuilder update_builder = message.update().with(message.id, "=", changed_message.id);
|
||||
switch (sp.get_name()) {
|
||||
case "stanza_id":
|
||||
update_builder.set(message.stanza_id, changed_message.stanza_id); break;
|
||||
case "counterpart":
|
||||
update_builder.set(message.counterpart_id, get_jid_id(changed_message.counterpart));
|
||||
update_builder.set(message.counterpart_resource, changed_message.counterpart.resourcepart); break;
|
||||
case "ourpart":
|
||||
update_builder.set(message.our_resource, changed_message.ourpart.resourcepart); break;
|
||||
case "direction":
|
||||
update_builder.set(message.direction, changed_message.direction); break;
|
||||
case "type_":
|
||||
update_builder.set(message.type_, changed_message.type_); break;
|
||||
case "time":
|
||||
update_builder.set(message.time, (long) changed_message.time.to_unix()); break;
|
||||
case "local_time":
|
||||
update_builder.set(message.local_time, (long) changed_message.local_time.to_unix()); break;
|
||||
case "body":
|
||||
update_builder.set(message.body, changed_message.body); break;
|
||||
case "encryption":
|
||||
update_builder.set(message.encryption, changed_message.encryption); break;
|
||||
case "marked":
|
||||
update_builder.set(message.marked, changed_message.marked); break;
|
||||
}
|
||||
update_builder.perform();
|
||||
|
||||
if (sp.get_name() == "real_jid") {
|
||||
real_jid.insert()
|
||||
.value(real_jid.message_id, changed_message.id)
|
||||
.value(real_jid.real_jid, changed_message.real_jid)
|
||||
.perform();
|
||||
}
|
||||
}
|
||||
|
||||
public Gee.List<Message> get_messages(Jid jid, Account account, int count, Message? before) {
|
||||
string jid_id = get_jid_id(jid).to_string();
|
||||
|
||||
QueryBuilder select = message.select()
|
||||
.with(message.counterpart_id, "=", get_jid_id(jid))
|
||||
.with(message.account_id, "=", account.id)
|
||||
.order_by(message.id, "DESC")
|
||||
.limit(count);
|
||||
if (before != null) {
|
||||
select.with(message.time, "<", (long) before.time.to_unix());
|
||||
}
|
||||
|
||||
LinkedList<Message> ret = new LinkedList<Message>();
|
||||
foreach (Row row in select) {
|
||||
ret.insert(0, get_message_from_row(row));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool contains_message(Message query_message, Account account) {
|
||||
int jid_id = get_jid_id(query_message.counterpart);
|
||||
return message.select()
|
||||
.with(message.account_id, "=", account.id)
|
||||
.with(message.stanza_id, "=", query_message.stanza_id)
|
||||
.with(message.counterpart_id, "=", jid_id)
|
||||
.with(message.counterpart_resource, "=", query_message.counterpart.resourcepart)
|
||||
.count() > 0;
|
||||
}
|
||||
|
||||
public bool contains_message_by_stanza_id(string stanza_id) {
|
||||
return message.select()
|
||||
.with(message.stanza_id, "=", stanza_id)
|
||||
.count() > 0;
|
||||
}
|
||||
|
||||
public Message? get_message_by_id(int id) {
|
||||
Row? row = message.row_with(message.id, id);
|
||||
if (row != null) {
|
||||
return get_message_from_row(row);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Message get_message_from_row(Row row) {
|
||||
Message new_message = new Message();
|
||||
|
||||
new_message.id = row[message.id];
|
||||
new_message.stanza_id = row[message.stanza_id];
|
||||
string from = get_jid_by_id(row[message.counterpart_id]);
|
||||
string from_resource = row[message.counterpart_resource];
|
||||
if (from_resource != null) {
|
||||
new_message.counterpart = new Jid(from + "/" + from_resource);
|
||||
} else {
|
||||
new_message.counterpart = new Jid(from);
|
||||
}
|
||||
new_message.direction = row[message.direction];
|
||||
new_message.type_ = (Message.Type) row[message.type_];
|
||||
new_message.time = new DateTime.from_unix_utc(row[message.time]);
|
||||
new_message.body = row[message.body];
|
||||
new_message.account = get_account_by_id(row[message.account_id]); // TODO dont have to generate acc new
|
||||
new_message.marked = (Message.Marked) row[message.marked];
|
||||
new_message.encryption = (Message.Encryption) row[message.encryption];
|
||||
new_message.real_jid = get_real_jid_for_message(new_message);
|
||||
return new_message;
|
||||
}
|
||||
|
||||
public string? get_real_jid_for_message(Message message) {
|
||||
return real_jid.select({real_jid.real_jid}).with(real_jid.message_id, "=", message.id)[real_jid.real_jid];
|
||||
}
|
||||
|
||||
public void add_conversation(Conversation new_conversation) {
|
||||
var insert = conversation.insert()
|
||||
.value(conversation.jid_id, get_jid_id(new_conversation.counterpart))
|
||||
.value(conversation.account_id, new_conversation.account.id)
|
||||
.value(conversation.type_, new_conversation.type_)
|
||||
.value(conversation.encryption, new_conversation.encryption)
|
||||
//.value(conversation.read_up_to, new_conversation.read_up_to)
|
||||
.value(conversation.active, new_conversation.active);
|
||||
if (new_conversation.last_active != null) {
|
||||
insert.value(conversation.last_active, (long) new_conversation.last_active.to_unix());
|
||||
} else {
|
||||
insert.value_null(conversation.last_active);
|
||||
}
|
||||
new_conversation.id = (int) insert.perform();
|
||||
new_conversation.notify.connect(on_conversation_update);
|
||||
}
|
||||
|
||||
public ArrayList<Conversation> get_conversations(Account account) {
|
||||
ArrayList<Conversation> ret = new ArrayList<Conversation>();
|
||||
foreach (Row row in conversation.select().with(conversation.account_id, "=", account.id)) {
|
||||
ret.add(get_conversation_from_row(row));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void on_conversation_update(Object o, ParamSpec sp) {
|
||||
Conversation changed_conversation = (Conversation) o;
|
||||
var update = conversation.update().with(conversation.jid_id, "=", get_jid_id(changed_conversation.counterpart)).with(conversation.account_id, "=", changed_conversation.account.id)
|
||||
.set(conversation.type_, changed_conversation.type_)
|
||||
.set(conversation.encryption, changed_conversation.encryption)
|
||||
//.set(conversation.read_up_to, changed_conversation.read_up_to)
|
||||
.set(conversation.active, changed_conversation.active);
|
||||
if (changed_conversation.last_active != null) {
|
||||
update.set(conversation.last_active, (long) changed_conversation.last_active.to_unix());
|
||||
} else {
|
||||
update.set_null(conversation.last_active);
|
||||
}
|
||||
update.perform();
|
||||
}
|
||||
|
||||
private Conversation get_conversation_from_row(Row row) {
|
||||
Conversation new_conversation = new Conversation(new Jid(get_jid_by_id(row[conversation.jid_id])), get_account_by_id(row[conversation.account_id]));
|
||||
|
||||
new_conversation.id = row[conversation.id];
|
||||
new_conversation.active = row[conversation.active];
|
||||
int64? last_active = row[conversation.last_active];
|
||||
if (last_active != null) new_conversation.last_active = new DateTime.from_unix_utc(last_active);
|
||||
new_conversation.type_ = row[conversation.type_];
|
||||
new_conversation.encryption = row[conversation.encryption];
|
||||
int? read_up_to = row[conversation.read_up_to];
|
||||
if (read_up_to != null) new_conversation.read_up_to = get_message_by_id(read_up_to);
|
||||
|
||||
new_conversation.notify.connect(on_conversation_update);
|
||||
return new_conversation;
|
||||
}
|
||||
|
||||
public void set_avatar_hash(Jid jid, string hash, int type) {
|
||||
avatar.insert().or("REPLACE")
|
||||
.value(avatar.jid, jid.to_string())
|
||||
.value(avatar.hash, hash)
|
||||
.value(avatar.type_, type)
|
||||
.perform();
|
||||
}
|
||||
|
||||
public HashMap<Jid, string> get_avatar_hashes(int type) {
|
||||
HashMap<Jid, string> ret = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
|
||||
foreach (Row row in avatar.select({avatar.jid, avatar.hash}).with(avatar.type_, "=", type)) {
|
||||
ret[new Jid(row[avatar.jid])] = row[avatar.hash];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void set_pgp_key(Jid jid, string key) {
|
||||
pgp.insert().or("REPLACE")
|
||||
.value(pgp.jid, jid.to_string())
|
||||
.value(pgp.key, key)
|
||||
.perform();
|
||||
}
|
||||
|
||||
public string? get_pgp_key(Jid jid) {
|
||||
return pgp.select({pgp.key}).with(pgp.jid, "=", jid.to_string())[pgp.key];
|
||||
}
|
||||
|
||||
public void add_entity_features(string entity, ArrayList<string> features) {
|
||||
foreach (string feature in features) {
|
||||
entity_feature.insert()
|
||||
.value(entity_feature.entity, entity)
|
||||
.value(entity_feature.feature, feature)
|
||||
.perform();
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<string> get_entity_features(string entity) {
|
||||
ArrayList<string> ret = new ArrayList<string>();
|
||||
foreach (Row row in entity_feature.select({entity_feature.feature}).with(entity_feature.entity, "=", entity)) {
|
||||
ret.add(row[entity_feature.feature]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
private int get_jid_id(Jid jid_obj) {
|
||||
Row? row = jid.row_with(jid.bare_jid, jid_obj.bare_jid.to_string());
|
||||
return row != null ? row[jid.id] : add_jid(jid_obj);
|
||||
}
|
||||
|
||||
private string? get_jid_by_id(int id) {
|
||||
return jid.select({jid.bare_jid}).with(jid.id, "=", id)[jid.bare_jid];
|
||||
}
|
||||
|
||||
private int add_jid(Jid jid_obj) {
|
||||
return (int) jid.insert().value(jid.bare_jid, jid_obj.bare_jid.to_string()).perform();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
23
client/src/service/entity_capabilities_storage.vala
Normal file
|
@ -0,0 +1,23 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class EntityCapabilitiesStorage : Xep.EntityCapabilities.Storage, Object {
|
||||
|
||||
private Database db;
|
||||
|
||||
public EntityCapabilitiesStorage(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public void store_features(string entity, ArrayList<string> features) {
|
||||
db.add_entity_features(entity, features);
|
||||
}
|
||||
|
||||
public ArrayList<string> get_features(string entitiy) {
|
||||
return db.get_entity_features(entitiy);
|
||||
}
|
||||
}
|
||||
}
|
166
client/src/service/message_manager.vala
Normal file
|
@ -0,0 +1,166 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class MessageManager : StreamInteractionModule, Object {
|
||||
public const string id = "message_manager";
|
||||
|
||||
public signal void pre_message_received(Entities.Message message, Conversation conversation);
|
||||
public signal void message_received(Entities.Message message, Conversation conversation);
|
||||
public signal void message_sent(Entities.Message message, Conversation conversation);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Database db;
|
||||
private HashMap<Conversation, ArrayList<Entities.Message>> messages = new HashMap<Conversation, ArrayList<Entities.Message>>(Conversation.hash_func, Conversation.equals_func);
|
||||
|
||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||
MessageManager m = new MessageManager(stream_interactor, db);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private MessageManager(StreamInteractor stream_interactor, Database db) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
this.db = db;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
}
|
||||
|
||||
public void send_message(string text, Conversation conversation) {
|
||||
Entities.Message message = new Entities.Message();
|
||||
message.account = conversation.account;
|
||||
message.body = text;
|
||||
message.time = new DateTime.now_utc();
|
||||
message.local_time = new DateTime.now_utc();
|
||||
message.direction = Entities.Message.DIRECTION_SENT;
|
||||
message.counterpart = conversation.counterpart;
|
||||
message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart);
|
||||
|
||||
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
|
||||
|
||||
if (stream != null) {
|
||||
Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza();
|
||||
new_message.to = message.counterpart.to_string();
|
||||
new_message.body = message.body;
|
||||
if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
|
||||
new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
|
||||
} else {
|
||||
new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
|
||||
}
|
||||
if (conversation.encryption == Conversation.ENCRYPTION_PGP) {
|
||||
string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart);
|
||||
if (key_id != null) {
|
||||
bool encrypted = Xep.Pgp.Module.get_module(stream).encrypt(new_message, key_id);
|
||||
if (encrypted) message.encryption = Entities.Message.Encryption.PGP;
|
||||
}
|
||||
}
|
||||
Xmpp.Message.Module.get_module(stream).send_message(stream, new_message);
|
||||
message.stanza_id = new_message.id;
|
||||
message.stanza = new_message;
|
||||
db.add_message(message, conversation.account);
|
||||
} else {
|
||||
// save for resend
|
||||
}
|
||||
|
||||
conversation.last_active = message.time;
|
||||
add_message(message, conversation);
|
||||
message_sent(message, conversation);
|
||||
}
|
||||
|
||||
public Gee.List<Entities.Message>? get_messages(Conversation conversation) {
|
||||
if (messages.has_key(conversation) && messages[conversation].size > 0) {
|
||||
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, messages[conversation][0]);
|
||||
db_messages.add_all(messages[conversation]);
|
||||
return db_messages;
|
||||
} else {
|
||||
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, null);
|
||||
return db_messages;
|
||||
}
|
||||
}
|
||||
|
||||
public Entities.Message? get_last_message(Conversation conversation) {
|
||||
if (messages.has_key(conversation) && messages[conversation].size > 0) {
|
||||
return messages[conversation][messages[conversation].size - 1];
|
||||
} else {
|
||||
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 1, null);
|
||||
if (db_messages.size >= 1) {
|
||||
return db_messages[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Gee.List<Entities.Message>? get_messages_before(Conversation? conversation, Entities.Message before) {
|
||||
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 20, before);
|
||||
return db_messages;
|
||||
}
|
||||
|
||||
public string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static MessageManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (MessageManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.message_modules[account].received_message.connect( (stream, message) => {
|
||||
on_message_received(account, message);
|
||||
});
|
||||
}
|
||||
|
||||
private void on_message_received(Account account, Xmpp.Message.Stanza message) {
|
||||
if (message.body == null) return;
|
||||
|
||||
Entities.Message new_message = new Entities.Message();
|
||||
new_message.account = account;
|
||||
new_message.stanza_id = message.id;
|
||||
Jid from_jid = new Jid(message.from);
|
||||
if (!account.bare_jid.equals_bare(from_jid) ||
|
||||
MucManager.get_instance(stream_interactor).get_nick(from_jid.bare_jid, account) == from_jid.resourcepart) {
|
||||
new_message.direction = Entities.Message.DIRECTION_RECEIVED;
|
||||
} else {
|
||||
new_message.direction = Entities.Message.DIRECTION_SENT;
|
||||
}
|
||||
new_message.counterpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.to) : new Jid(message.from);
|
||||
new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.from) : new Jid(message.to);
|
||||
new_message.body = message.body;
|
||||
new_message.stanza = message;
|
||||
new_message.set_type_string(message.type_);
|
||||
new_message.time = Xep.DelayedDelivery.Module.get_send_time(message);
|
||||
if (new_message.time == null) {
|
||||
new_message.time = new DateTime.now_utc();
|
||||
}
|
||||
new_message.local_time = new DateTime.now_utc();
|
||||
if (Xep.Pgp.MessageFlag.get_flag(message) != null) {
|
||||
new_message.encryption = Entities.Message.Encryption.PGP;
|
||||
}
|
||||
Conversation conversation = ConversationManager.get_instance(stream_interactor).get_add_conversation(new_message.counterpart, account);
|
||||
pre_message_received(new_message, conversation);
|
||||
|
||||
bool is_uuid = new_message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", new_message.stanza_id);
|
||||
if ((is_uuid && !db.contains_message_by_stanza_id(new_message.stanza_id)) ||
|
||||
(!is_uuid && !db.contains_message(new_message, conversation.account))) {
|
||||
db.add_message(new_message, conversation.account);
|
||||
add_message(new_message, conversation);
|
||||
if (new_message.time.difference(conversation.last_active) > 0) {
|
||||
conversation.last_active = new_message.time;
|
||||
}
|
||||
if (new_message.direction == Entities.Message.DIRECTION_SENT) {
|
||||
message_sent(new_message, conversation);
|
||||
} else {
|
||||
message_received(new_message, conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void add_message(Entities.Message message, Conversation conversation) {
|
||||
if (!messages.has_key(conversation)) {
|
||||
messages[conversation] = new ArrayList<Entities.Message>(Entities.Message.equals_func);
|
||||
}
|
||||
messages[conversation].add(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
96
client/src/service/module_manager.vala
Normal file
|
@ -0,0 +1,96 @@
|
|||
using Gee;
|
||||
|
||||
using Dino.Entities;
|
||||
using Xmpp;
|
||||
|
||||
namespace Dino {
|
||||
|
||||
public class ModuleManager {
|
||||
|
||||
public HashMap<Account, Tls.Module> tls_modules = new HashMap<Account, Tls.Module>();
|
||||
public HashMap<Account, PlainSasl.Module> plain_sasl_modules = new HashMap<Account, PlainSasl.Module>();
|
||||
public HashMap<Account, Bind.Module> bind_modules = new HashMap<Account, Bind.Module>();
|
||||
public HashMap<Account, Roster.Module> roster_modules = new HashMap<Account, Roster.Module>();
|
||||
public HashMap<Account, Xep.ServiceDiscovery.Module> service_discovery_modules = new HashMap<Account, Xep.ServiceDiscovery.Module>();
|
||||
public HashMap<Account, Xep.PrivateXmlStorage.Module> private_xmp_storage_modules = new HashMap<Account, Xep.PrivateXmlStorage.Module>();
|
||||
public HashMap<Account, Xep.Bookmarks.Module> bookmarks_module = new HashMap<Account, Xep.Bookmarks.Module>();
|
||||
public HashMap<Account, Presence.Module> presence_modules = new HashMap<Account, Presence.Module>();
|
||||
public HashMap<Account, Xmpp.Message.Module> message_modules = new HashMap<Account, Xmpp.Message.Module>();
|
||||
public HashMap<Account, Xep.MessageCarbons.Module> message_carbons_modules = new HashMap<Account, Xep.MessageCarbons.Module>();
|
||||
public HashMap<Account, Xep.Muc.Module> muc_modules = new HashMap<Account, Xep.Muc.Module>();
|
||||
public HashMap<Account, Xep.Pgp.Module> pgp_modules = new HashMap<Account, Xep.Pgp.Module>();
|
||||
public HashMap<Account, Xep.Pubsub.Module> pubsub_modules = new HashMap<Account, Xep.Pubsub.Module>();
|
||||
public HashMap<Account, Xep.EntityCapabilities.Module> entity_capabilities_modules = new HashMap<Account, Xep.EntityCapabilities.Module>();
|
||||
public HashMap<Account, Xep.UserAvatars.Module> user_avatars_modules = new HashMap<Account, Xep.UserAvatars.Module>();
|
||||
public HashMap<Account, Xep.VCard.Module> vcard_modules = new HashMap<Account, Xep.VCard.Module>();
|
||||
public HashMap<Account, Xep.MessageDeliveryReceipts.Module> message_delivery_receipts_modules = new HashMap<Account, Xep.MessageDeliveryReceipts.Module>();
|
||||
public HashMap<Account, Xep.ChatStateNotifications.Module> chat_state_notifications_modules = new HashMap<Account, Xep.ChatStateNotifications.Module>();
|
||||
public HashMap<Account, Xep.ChatMarkers.Module> chat_markers_modules = new HashMap<Account, Xep.ChatMarkers.Module>();
|
||||
public HashMap<Account, Xep.Ping.Module> ping_modules = new HashMap<Account, Xep.Ping.Module>();
|
||||
public HashMap<Account, Xep.DelayedDelivery.Module> delayed_delivery_module = new HashMap<Account, Xep.DelayedDelivery.Module>();
|
||||
public HashMap<Account, StreamError.Module> stream_error_modules = new HashMap<Account, StreamError.Module>();
|
||||
|
||||
private AvatarStorage avatar_storage = new AvatarStorage("./");
|
||||
private EntityCapabilitiesStorage entity_capabilities_storage;
|
||||
|
||||
public ModuleManager(Database db) {
|
||||
entity_capabilities_storage = new EntityCapabilitiesStorage(db);
|
||||
}
|
||||
|
||||
public ArrayList<Core.XmppStreamModule> get_modules(Account account, string? resource = null) {
|
||||
ArrayList<Core.XmppStreamModule> modules = new ArrayList<Core.XmppStreamModule>();
|
||||
|
||||
if (!tls_modules.has_key(account)) add_account(account);
|
||||
|
||||
modules.add(tls_modules[account]);
|
||||
modules.add(plain_sasl_modules[account]);
|
||||
modules.add(new Bind.Module(resource == null ? account.resourcepart : resource));
|
||||
modules.add(roster_modules[account]);
|
||||
modules.add(service_discovery_modules[account]);
|
||||
modules.add(private_xmp_storage_modules[account]);
|
||||
modules.add(bookmarks_module[account]);
|
||||
modules.add(presence_modules[account]);
|
||||
modules.add(message_modules[account]);
|
||||
modules.add(message_carbons_modules[account]);
|
||||
modules.add(muc_modules[account]);
|
||||
modules.add(pgp_modules[account]);
|
||||
modules.add(pubsub_modules[account]);
|
||||
modules.add(entity_capabilities_modules[account]);
|
||||
modules.add(user_avatars_modules[account]);
|
||||
modules.add(vcard_modules[account]);
|
||||
modules.add(message_delivery_receipts_modules[account]);
|
||||
modules.add(chat_state_notifications_modules[account]);
|
||||
modules.add(chat_markers_modules[account]);
|
||||
modules.add(ping_modules[account]);
|
||||
modules.add(delayed_delivery_module[account]);
|
||||
modules.add(stream_error_modules[account]);
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void add_account(Account account) {
|
||||
tls_modules[account] = new Tls.Module();
|
||||
plain_sasl_modules[account] = new PlainSasl.Module(account.bare_jid.to_string(), account.password);
|
||||
bind_modules[account] = new Bind.Module(account.resourcepart);
|
||||
roster_modules[account] = new Roster.Module();
|
||||
service_discovery_modules[account] = new Xep.ServiceDiscovery.Module.with_identity("client", "pc");
|
||||
private_xmp_storage_modules[account] = new Xep.PrivateXmlStorage.Module();
|
||||
bookmarks_module[account] = new Xep.Bookmarks.Module();
|
||||
presence_modules[account] = new Presence.Module();
|
||||
message_modules[account] = new Xmpp.Message.Module();
|
||||
message_carbons_modules[account] = new Xep.MessageCarbons.Module();
|
||||
muc_modules[account] = new Xep.Muc.Module();
|
||||
pgp_modules[account] = new Xep.Pgp.Module();
|
||||
pubsub_modules[account] = new Xep.Pubsub.Module();
|
||||
entity_capabilities_modules[account] = new Xep.EntityCapabilities.Module(entity_capabilities_storage);
|
||||
user_avatars_modules[account] = new Xep.UserAvatars.Module(avatar_storage);
|
||||
vcard_modules[account] = new Xep.VCard.Module(avatar_storage);
|
||||
message_delivery_receipts_modules[account] = new Xep.MessageDeliveryReceipts.Module();
|
||||
chat_state_notifications_modules[account] = new Xep.ChatStateNotifications.Module();
|
||||
chat_markers_modules[account] = new Xep.ChatMarkers.Module();
|
||||
ping_modules[account] = new Xep.Ping.Module();
|
||||
delayed_delivery_module[account] = new Xep.DelayedDelivery.Module();
|
||||
stream_error_modules[account] = new StreamError.Module();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
224
client/src/service/muc_manager.vala
Normal file
|
@ -0,0 +1,224 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class MucManager : StreamInteractionModule, Object {
|
||||
public const string id = "muc_manager";
|
||||
|
||||
public signal void groupchat_joined(Account account, Jid jid, string nick);
|
||||
public signal void groupchat_subject_set(Account account, Jid jid, string subject);
|
||||
public signal void bookmarks_updated(Account account, ArrayList<Xep.Bookmarks.Conference> conferences);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
protected HashMap<Jid, Xep.Bookmarks.Conference> conference_bookmarks = new HashMap<Jid, Xep.Bookmarks.Conference>();
|
||||
|
||||
public static void start(StreamInteractor stream_interactor) {
|
||||
MucManager m = new MucManager(stream_interactor);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private MucManager(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
|
||||
MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_pre_message_received);
|
||||
}
|
||||
|
||||
public void join(Account account, Jid jid, string nick) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Muc.Module.get_module(stream).enter(stream, jid.bare_jid.to_string(), nick, null, new MucEnterListenerImpl(this, jid, nick, account));
|
||||
}
|
||||
|
||||
public void part(Account account, Jid jid) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Muc.Module.get_module(stream).exit(stream, jid.bare_jid.to_string());
|
||||
}
|
||||
|
||||
public void change_subject(Account account, Jid jid, string subject) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Muc.Module.get_module(stream).change_subject(stream, jid.bare_jid.to_string(), subject);
|
||||
}
|
||||
|
||||
public void change_nick(Account account, Jid jid, string new_nick) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Muc.Module.get_module(stream).change_nick(stream, jid.bare_jid.to_string(), new_nick);
|
||||
}
|
||||
|
||||
public void kick(Account account, Jid jid, string nick) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Muc.Module.get_module(stream).kick(stream, jid.bare_jid.to_string(), nick);
|
||||
}
|
||||
|
||||
public ArrayList<Jid>? get_occupants(Jid jid, Account account) {
|
||||
return PresenceManager.get_instance(stream_interactor).get_full_jids(jid, account);
|
||||
}
|
||||
|
||||
public ArrayList<Jid>? get_other_occupants(Jid jid, Account account) {
|
||||
ArrayList<Jid>? occupants = get_occupants(jid, account);
|
||||
string? nick = get_nick(jid, account);
|
||||
if (occupants != null && nick != null) {
|
||||
occupants.remove(new Jid(@"$(jid.bare_jid)/$nick"));
|
||||
}
|
||||
return occupants;
|
||||
}
|
||||
|
||||
public bool is_groupchat(Jid jid, Account account) {
|
||||
Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
|
||||
return !jid.is_full() && conversation != null && conversation.type_ == Conversation.TYPE_GROUPCHAT;
|
||||
}
|
||||
|
||||
public bool is_groupchat_occupant(Jid jid, Account account) {
|
||||
return is_groupchat(jid.bare_jid, account) && jid.is_full();
|
||||
}
|
||||
|
||||
public void get_bookmarks(Account account, Xep.Bookmarks.ConferencesRetrieveResponseListener listener) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void add_bookmark(Account account, Xep.Bookmarks.Conference conference) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xep.Bookmarks.Module.get_module(stream).add_conference(stream, conference);
|
||||
}
|
||||
}
|
||||
|
||||
public void replace_bookmark(Account account, Xep.Bookmarks.Conference was, Xep.Bookmarks.Conference replace) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xep.Bookmarks.Module.get_module(stream).replace_conference(stream, was, replace);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove_bookmark(Account account, Xep.Bookmarks.Conference conference) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xep.Bookmarks.Module.get_module(stream).remove_conference(stream, conference);
|
||||
}
|
||||
}
|
||||
|
||||
public string? get_groupchat_subject(Jid jid, Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
return Xep.Muc.Flag.get_flag(stream).get_muc_subject(jid.bare_jid.to_string());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Jid? get_real_jid(Jid jid, Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(jid.to_string());
|
||||
if (real_jid != null) {
|
||||
return new Jid(real_jid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Jid? get_message_real_jid(Entities.Message message) {
|
||||
if (message.real_jid != null) {
|
||||
return new Jid(message.real_jid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? get_nick(Jid jid, Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
return Xep.Muc.Flag.get_flag(stream).get_muc_nick(jid.bare_jid.to_string());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MucManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (MucManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.muc_modules[account].subject_set.connect( (stream, subject, jid) => {
|
||||
on_subject_set(account, new Jid(jid), subject);
|
||||
});
|
||||
stream_interactor.module_manager.bookmarks_module[account].conferences_updated.connect( (stream, conferences) => {
|
||||
bookmarks_updated(account, conferences);
|
||||
});
|
||||
}
|
||||
|
||||
private void on_subject_set(Account account, Jid sender_jid, string subject) {
|
||||
groupchat_subject_set(account, sender_jid, subject);
|
||||
}
|
||||
|
||||
private void on_stream_negotiated(Account account) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xep.Bookmarks.Module.get_module(stream).get_conferences(stream, new BookmarksRetrieveResponseListener(this, account));
|
||||
}
|
||||
|
||||
private void on_pre_message_received(Entities.Message message, Conversation conversation) {
|
||||
if (conversation.type_ != Conversation.TYPE_GROUPCHAT) return;
|
||||
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
|
||||
if (stream == null) return;
|
||||
if (Xep.DelayedDelivery.MessageFlag.get_flag(message.stanza) == null) {
|
||||
string? real_jid = Xep.Muc.Flag.get_flag(stream).get_real_jid(message.counterpart.to_string());
|
||||
if (real_jid != null && real_jid != message.counterpart.to_string()) {
|
||||
message.real_jid = real_jid;
|
||||
}
|
||||
}
|
||||
string muc_nick = Xep.Muc.Flag.get_flag(stream).get_muc_nick(conversation.counterpart.bare_jid.to_string());
|
||||
if (message.from.equals(new Jid(@"$(message.from.bare_jid)/$muc_nick"))) { // TODO better from own
|
||||
Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
|
||||
if (messages != null) { // TODO not here
|
||||
foreach (Entities.Message m in messages) {
|
||||
if (m.equals(message)) {
|
||||
m.marked = Entities.Message.Marked.RECEIVED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BookmarksRetrieveResponseListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object {
|
||||
MucManager outer = null;
|
||||
Account account = null;
|
||||
|
||||
public BookmarksRetrieveResponseListener(MucManager outer, Account account) {
|
||||
this.outer = outer;
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public void on_result(Core.XmppStream stream, ArrayList<Xep.Bookmarks.Conference> conferences) {
|
||||
foreach (Xep.Bookmarks.Conference bookmark in conferences) {
|
||||
Jid jid = new Jid(bookmark.jid);
|
||||
outer.conference_bookmarks[jid] = bookmark;
|
||||
if (bookmark.autojoin) {
|
||||
outer.join(account, jid, bookmark.nick);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MucEnterListenerImpl : Xep.Muc.MucEnterListener, Object { // TODO
|
||||
private MucManager outer;
|
||||
private Jid jid;
|
||||
private string nick;
|
||||
private Account account;
|
||||
public MucEnterListenerImpl(MucManager outer, Jid jid, string nick, Account account) {
|
||||
this.outer = outer;
|
||||
this.jid = jid;
|
||||
this.nick = nick;
|
||||
this.account = account;
|
||||
}
|
||||
public void on_success() {
|
||||
outer.groupchat_joined(account, jid, nick);
|
||||
}
|
||||
public void on_error(Xep.Muc.MucEnterError error) { }
|
||||
}
|
||||
}
|
||||
}
|
54
client/src/service/pgp_manager.vala
Normal file
|
@ -0,0 +1,54 @@
|
|||
using Gee;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class PgpManager : StreamInteractionModule, Object {
|
||||
public const string id = "pgp_manager";
|
||||
|
||||
public const string MESSAGE_ENCRYPTED = "pgp";
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Database db;
|
||||
private HashMap<Jid, string> pgp_key_ids = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
|
||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||
PgpManager m = new PgpManager(stream_interactor, db);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private PgpManager(StreamInteractor stream_interactor, Database db) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
this.db = db;
|
||||
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
}
|
||||
|
||||
public string? get_key_id(Account account, Jid jid) {
|
||||
return db.get_pgp_key(jid);
|
||||
}
|
||||
|
||||
public static PgpManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (PgpManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.pgp_modules[account].received_jid_key_id.connect((stream, jid, key_id) => {
|
||||
on_jid_key_received(account, new Jid(jid), key_id);
|
||||
});
|
||||
}
|
||||
|
||||
private void on_jid_key_received(Account account, Jid jid, string key_id) {
|
||||
if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) {
|
||||
if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
|
||||
db.set_pgp_key(jid.bare_jid, key_id);
|
||||
}
|
||||
}
|
||||
pgp_key_ids[jid] = key_id;
|
||||
}
|
||||
}
|
||||
}
|
150
client/src/service/presence_manager.vala
Normal file
|
@ -0,0 +1,150 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class PresenceManager : StreamInteractionModule, Object {
|
||||
public const string id = "presence_manager";
|
||||
|
||||
public signal void show_received(Show show, Jid jid, Account account);
|
||||
public signal void received_subscription_request(Jid jid, Account account);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private HashMap<Jid, HashMap<Jid, ArrayList<Show>>> shows = new HashMap<Jid, HashMap<Jid, ArrayList<Show>>>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
private HashMap<Jid, ArrayList<Jid>> resources = new HashMap<Jid, ArrayList<Jid>>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||
|
||||
public static void start(StreamInteractor stream_interactor) {
|
||||
PresenceManager m = new PresenceManager(stream_interactor);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
private PresenceManager(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
}
|
||||
|
||||
public Show get_last_show(Jid jid, Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
Xmpp.Presence.Stanza? presence = Xmpp.Presence.Flag.get_flag(stream).get_presence(jid.to_string());
|
||||
if (presence != null) {
|
||||
return new Show(jid, presence.show, new DateTime.now_local());
|
||||
}
|
||||
}
|
||||
return new Show(jid, Show.OFFLINE, new DateTime.now_local());
|
||||
}
|
||||
|
||||
public HashMap<Jid, ArrayList<Show>>? get_shows(Jid jid, Account account) {
|
||||
return shows[jid];
|
||||
}
|
||||
|
||||
public ArrayList<Jid>? get_full_jids(Jid jid, Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
ArrayList<string> resources = Xmpp.Presence.Flag.get_flag(stream).get_resources(jid.bare_jid.to_string());
|
||||
if (resources == null) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<Jid> ret = new ArrayList<Jid>(Jid.equals_func);
|
||||
foreach (string resource in resources) {
|
||||
ret.add(new Jid(resource));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void request_subscription(Account account, Jid jid) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xmpp.Presence.Module.get_module(stream).request_subscription(stream, jid.bare_jid.to_string());
|
||||
}
|
||||
|
||||
public void approve_subscription(Account account, Jid jid) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xmpp.Presence.Module.get_module(stream).approve_subscription(stream, jid.bare_jid.to_string());
|
||||
}
|
||||
|
||||
public void deny_subscription(Account account, Jid jid) {
|
||||
Core.XmppStream stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xmpp.Presence.Module.get_module(stream).deny_subscription(stream, jid.bare_jid.to_string());
|
||||
}
|
||||
|
||||
public static PresenceManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (PresenceManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.presence_modules[account].received_available_show.connect((stream, jid, show) =>
|
||||
on_received_available_show(account, new Jid(jid), show)
|
||||
);
|
||||
stream_interactor.module_manager.presence_modules[account].received_unavailable.connect((stream, jid) =>
|
||||
on_received_unavailable(account, new Jid(jid))
|
||||
);
|
||||
stream_interactor.module_manager.presence_modules[account].received_subscription_request.connect((stream, jid) =>
|
||||
received_subscription_request(new Jid(jid), account)
|
||||
);
|
||||
}
|
||||
|
||||
private void on_received_available_show(Account account, Jid jid, string show) {
|
||||
lock (resources) {
|
||||
if (!resources.has_key(jid)){
|
||||
resources[jid] = new ArrayList<Jid>(Jid.equals_func);
|
||||
}
|
||||
if (!resources[jid].contains(jid)) {
|
||||
resources[jid].add(jid);
|
||||
}
|
||||
}
|
||||
add_show(account, jid, show);
|
||||
}
|
||||
|
||||
private void on_received_unavailable(Account account, Jid jid) {
|
||||
lock (resources) {
|
||||
if (resources.has_key(jid)) {
|
||||
resources[jid].remove(jid);
|
||||
if (resources[jid].size == 0 || jid.is_bare()) {
|
||||
resources.unset(jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
add_show(account, jid, Show.OFFLINE);
|
||||
}
|
||||
|
||||
private void add_show(Account account, Jid jid, string s) {
|
||||
Show show = new Show(jid, s, new DateTime.now_local());
|
||||
lock (shows) {
|
||||
if (!shows.has_key(jid)) {
|
||||
shows[jid] = new HashMap<Jid, ArrayList<Show>>();
|
||||
}
|
||||
if (!shows[jid].has_key(jid)) {
|
||||
shows[jid][jid] = new ArrayList<Show>();
|
||||
}
|
||||
shows[jid][jid].add(show);
|
||||
}
|
||||
show_received(show, jid, account);
|
||||
}
|
||||
}
|
||||
|
||||
public class Show : Object {
|
||||
public const string ONLINE = Xmpp.Presence.Stanza.SHOW_ONLINE;
|
||||
public const string AWAY = Xmpp.Presence.Stanza.SHOW_AWAY;
|
||||
public const string CHAT = Xmpp.Presence.Stanza.SHOW_CHAT;
|
||||
public const string DND = Xmpp.Presence.Stanza.SHOW_DND;
|
||||
public const string XA = Xmpp.Presence.Stanza.SHOW_XA;
|
||||
public const string OFFLINE = "offline";
|
||||
|
||||
public Jid jid;
|
||||
public string as;
|
||||
public DateTime datetime;
|
||||
|
||||
public Show(Jid jid, string show, DateTime datetime) {
|
||||
this.jid = jid;
|
||||
this.as = show;
|
||||
this.datetime = datetime;
|
||||
}
|
||||
}
|
||||
}
|
82
client/src/service/roster_manager.vala
Normal file
|
@ -0,0 +1,82 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class RosterManager : StreamInteractionModule, Object {
|
||||
public const string id = "roster_manager";
|
||||
|
||||
public signal void removed_roster_item(Account account, Jid jid, Roster.Item roster_item);
|
||||
public signal void updated_roster_item(Account account, Jid jid, Roster.Item roster_item);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
|
||||
public static void start(StreamInteractor stream_interactor) {
|
||||
RosterManager m = new RosterManager(stream_interactor);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
|
||||
public RosterManager(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
}
|
||||
|
||||
public ArrayList<Roster.Item> get_roster(Account account) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
ArrayList<Roster.Item> ret = new ArrayList<Roster.Item>();
|
||||
if (stream != null) {
|
||||
ret.add_all(Xmpp.Roster.Flag.get_flag(stream).get_roster());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Roster.Item? get_roster_item(Account account, Jid jid) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) {
|
||||
return Xmpp.Roster.Flag.get_flag(stream).get_item(jid.bare_jid.to_string());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void remove_jid(Account account, Jid jid) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xmpp.Roster.Module.get_module(stream).remove_jid(stream, jid.bare_jid.to_string());
|
||||
}
|
||||
|
||||
public void add_jid(Account account, Jid jid, string? handle) {
|
||||
Core.XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream != null) Xmpp.Roster.Module.get_module(stream).add_jid(stream, jid.bare_jid.to_string(), handle);
|
||||
}
|
||||
|
||||
public static RosterManager? get_instance(StreamInteractor stream_interactor) {
|
||||
return (RosterManager) stream_interactor.get_module(id);
|
||||
}
|
||||
|
||||
internal string get_id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void on_account_added(Account account) {
|
||||
stream_interactor.module_manager.roster_modules[account].received_roster.connect( (stream, roster) => {
|
||||
on_roster_received(account, roster);
|
||||
});
|
||||
stream_interactor.module_manager.roster_modules[account].item_removed.connect( (stream, roster_item) => {
|
||||
removed_roster_item(account, new Jid(roster_item.jid), roster_item);
|
||||
});
|
||||
stream_interactor.module_manager.roster_modules[account].item_updated.connect( (stream, roster_item) => {
|
||||
on_roster_item_updated(account, roster_item);
|
||||
});
|
||||
}
|
||||
|
||||
private void on_roster_received(Account account, Collection<Roster.Item> roster_items) {
|
||||
foreach (Roster.Item roster_item in roster_items) {
|
||||
on_roster_item_updated(account, roster_item);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_roster_item_updated(Account account, Roster.Item roster_item) {
|
||||
updated_roster_item(account, new Jid(roster_item.jid), roster_item);
|
||||
}
|
||||
}
|
||||
}
|
68
client/src/service/stream_interactor.vala
Normal file
|
@ -0,0 +1,68 @@
|
|||
using Gee;
|
||||
|
||||
using Xmpp;
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino {
|
||||
public class StreamInteractor {
|
||||
|
||||
public signal void account_added(Account account);
|
||||
public signal void stream_negotiated(Account account);
|
||||
|
||||
public ModuleManager module_manager;
|
||||
public ConnectionManager connection_manager;
|
||||
private ArrayList<StreamInteractionModule> interaction_modules = new ArrayList<StreamInteractionModule>();
|
||||
|
||||
public StreamInteractor(Database db) {
|
||||
module_manager = new ModuleManager(db);
|
||||
connection_manager = new ConnectionManager(module_manager);
|
||||
|
||||
connection_manager.stream_opened.connect(on_stream_opened);
|
||||
}
|
||||
|
||||
public void connect(Account account) {
|
||||
module_manager.add_account(account);
|
||||
account_added(account);
|
||||
connection_manager.connect(account);
|
||||
}
|
||||
|
||||
public void disconnect(Account account) {
|
||||
connection_manager.disconnect(account);
|
||||
}
|
||||
|
||||
public ArrayList<Account> get_accounts() {
|
||||
ArrayList<Account> ret = new ArrayList<Account>(Account.equals_func);
|
||||
foreach (Account account in connection_manager.get_managed_accounts()) {
|
||||
ret.add(account);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Core.XmppStream? get_stream(Account account) {
|
||||
return connection_manager.get_stream(account);
|
||||
}
|
||||
|
||||
public void add_module(StreamInteractionModule module) {
|
||||
interaction_modules.add(module);
|
||||
}
|
||||
|
||||
public StreamInteractionModule? get_module(string id) {
|
||||
foreach (StreamInteractionModule module in interaction_modules) {
|
||||
if (module.get_id() == id) {
|
||||
return module;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void on_stream_opened(Account account, Core.XmppStream stream) {
|
||||
stream.stream_negotiated.connect( (stream) => {
|
||||
stream_negotiated(account);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface StreamInteractionModule : Object {
|
||||
internal abstract string get_id();
|
||||
}
|
||||
}
|
28
client/src/settings.vala
Normal file
|
@ -0,0 +1,28 @@
|
|||
namespace Dino {
|
||||
|
||||
public class Settings {
|
||||
|
||||
private GLib.Settings gsettings;
|
||||
|
||||
public bool send_read {
|
||||
get { return gsettings.get_boolean("send-read"); }
|
||||
set { gsettings.set_boolean("send-read", value); }
|
||||
}
|
||||
|
||||
public bool convert_utf8_smileys {
|
||||
get { return gsettings.get_boolean("convert-utf8-smileys"); }
|
||||
set { gsettings.set_boolean("convert-utf8-smileys", value); }
|
||||
}
|
||||
|
||||
public Settings(GLib.Settings gsettings) {
|
||||
this.gsettings = gsettings;
|
||||
}
|
||||
|
||||
public static Settings instance() {
|
||||
SettingsSchemaSource sss = SettingsSchemaSource.get_default();
|
||||
SettingsSchema schema = sss.lookup("org.dino-im", false);
|
||||
return new Settings(new GLib.Settings.full(schema, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
67
client/src/ui/add_conversation/chat/add_contact_dialog.vala
Normal file
|
@ -0,0 +1,67 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
82
client/src/ui/add_conversation/chat/dialog.vala
Normal file
|
@ -0,0 +1,82 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
77
client/src/ui/add_conversation/chat/roster_list.vala
Normal file
|
@ -0,0 +1,77 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
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_press_event.connect_after(after_jid_entry_key_press);
|
||||
nick_entry.key_press_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 after_jid_entry_key_press() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
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_label.label; }
|
||||
set {
|
||||
jid_label.label = value;
|
||||
jid_entry.text = value;
|
||||
}
|
||||
}
|
||||
public string nick {
|
||||
get { return nick_label.label; }
|
||||
set {
|
||||
nick_label.label = value;
|
||||
nick_entry.text = value;
|
||||
}
|
||||
}
|
||||
public string password {
|
||||
get { return password_label.label; }
|
||||
set {
|
||||
password_label.label = value;
|
||||
password_entry.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
[GtkChild]
|
||||
private Stack accounts_stack;
|
||||
|
||||
[GtkChild]
|
||||
private Stack jid_stack;
|
||||
|
||||
[GtkChild]
|
||||
private Stack nick_stack;
|
||||
|
||||
[GtkChild]
|
||||
private Stack password_stack;
|
||||
|
||||
[GtkChild]
|
||||
private Button accounts_button;
|
||||
|
||||
[GtkChild]
|
||||
private Button jid_button;
|
||||
|
||||
[GtkChild]
|
||||
private Button nick_button;
|
||||
|
||||
[GtkChild]
|
||||
private Button password_button;
|
||||
|
||||
[GtkChild]
|
||||
private Label accounts_label;
|
||||
|
||||
[GtkChild]
|
||||
private Label jid_label;
|
||||
|
||||
[GtkChild]
|
||||
private Label nick_label;
|
||||
|
||||
[GtkChild]
|
||||
private Label password_label;
|
||||
|
||||
[GtkChild]
|
||||
private ComboBoxText accounts_comboboxtext;
|
||||
|
||||
[GtkChild]
|
||||
private Entry jid_entry;
|
||||
|
||||
[GtkChild]
|
||||
private Entry nick_entry;
|
||||
|
||||
[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_press_event.connect(() => { jid_label.label = jid_entry.text; return false; });
|
||||
nick_entry.key_press_event.connect(() => { nick_label.label = nick_entry.text; return false; });
|
||||
password_entry.key_press_event.connect(() => { password_label.label = password_entry.text; return false; });
|
||||
|
||||
jid_entry.key_press_event.connect(() => { done = true; return false; }); // just for notifying
|
||||
nick_entry.key_press_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 clear() {
|
||||
jid = "";
|
||||
nick = "";
|
||||
password = "";
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
105
client/src/ui/add_conversation/conference/conference_list.vala
Normal file
|
@ -0,0 +1,105 @@
|
|||
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, new BookmarksListener(this, stream_interactor, 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 class BookmarksListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object {
|
||||
ConferenceList outer;
|
||||
Account account;
|
||||
public BookmarksListener(ConferenceList outer, StreamInteractor stream_interactor, Account account) {
|
||||
this.outer = outer;
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public void on_result(Core.XmppStream stream, ArrayList<Xep.Bookmarks.Conference> conferences) {
|
||||
outer.lists[account] = conferences;
|
||||
Idle.add(() => { outer.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));
|
||||
}
|
||||
}
|
||||
}
|
165
client/src/ui/add_conversation/conference/dialog.vala
Normal file
|
@ -0,0 +1,165 @@
|
|||
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(show_conference_details_view);
|
||||
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();
|
||||
}
|
||||
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);
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
43
client/src/ui/add_conversation/list_row.vala
Normal file
|
@ -0,0 +1,43 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
124
client/src/ui/add_conversation/select_jid_fragment.vala
Normal file
|
@ -0,0 +1,124 @@
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
112
client/src/ui/application.vala
Normal file
|
@ -0,0 +1,112 @@
|
|||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
public class Dino.Ui.Application : Gtk.Application {
|
||||
|
||||
private Database db;
|
||||
private StreamInteractor stream_interaction;
|
||||
|
||||
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() {
|
||||
this.db = new Database("store.sqlite3");
|
||||
this.stream_interaction = new StreamInteractor(db);
|
||||
|
||||
AvatarManager.start(stream_interaction, db);
|
||||
MessageManager.start(stream_interaction, db);
|
||||
CounterpartInteractionManager.start(stream_interaction);
|
||||
PresenceManager.start(stream_interaction);
|
||||
MucManager.start(stream_interaction);
|
||||
PgpManager.start(stream_interaction, db);
|
||||
RosterManager.start(stream_interaction);
|
||||
ConversationManager.start(stream_interaction, db);
|
||||
ChatInteraction.start(stream_interaction);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
233
client/src/ui/avatar_generator.vala
Normal file
|
@ -0,0 +1,233 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
123
client/src/ui/chat_input.vala
Normal file
|
@ -0,0 +1,123 @@
|
|||
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 "/ping": // TODO remove this
|
||||
Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account))
|
||||
.send_ping(stream_interactor.get_stream(conversation.account), @"$(conversation.counterpart.bare_jid)/$(token[1])");
|
||||
Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account)).get_id();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
47
client/src/ui/conversation_list_titlebar.vala
Normal file
|
@ -0,0 +1,47 @@
|
|||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
[GtkTemplate (ui = "/org/dino-im/conversation_list_titlebar.ui")]
|
||||
public class Dino.Ui.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(ApplicationWindow application, StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
create_add_menu(application);
|
||||
}
|
||||
|
||||
private void create_add_menu(ApplicationWindow application) {
|
||||
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((ApplicationWindow) get_toplevel());
|
||||
add_chat_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation));
|
||||
add_chat_dialog.show();
|
||||
});
|
||||
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((ApplicationWindow) get_toplevel());
|
||||
add_conference_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation));
|
||||
add_conference_dialog.show();
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
88
client/src/ui/conversation_selector/chat_row.vala
Normal file
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
175
client/src/ui/conversation_selector/conversation_row.vala
Normal file
|
@ -0,0 +1,175 @@
|
|||
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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
33
client/src/ui/conversation_selector/groupchat_row.vala
Normal file
|
@ -0,0 +1,33 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
173
client/src/ui/conversation_selector/list.vala
Normal file
|
@ -0,0 +1,173 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
56
client/src/ui/conversation_selector/view.vala
Normal file
|
@ -0,0 +1,56 @@
|
|||
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_press_event.connect(search_key_press_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_press_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;
|
||||
}
|
||||
}
|
||||
}
|
164
client/src/ui/conversation_summary/merged_message_item.vala
Normal file
|
@ -0,0 +1,164 @@
|
|||
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 == Entities.Message.Encryption.PGP) {
|
||||
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.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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
30
client/src/ui/conversation_summary/merged_status_item.vala
Normal file
|
@ -0,0 +1,30 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
client/src/ui/conversation_summary/status_item.vala
Normal file
|
@ -0,0 +1,29 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
221
client/src/ui/conversation_summary/view.vala
Normal file
|
@ -0,0 +1,221 @@
|
|||
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;
|
||||
}
|
||||
|
||||
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); });
|
||||
}
|
||||
}
|
||||
}
|
124
client/src/ui/conversation_titlebar.vala
Normal file
|
@ -0,0 +1,124 @@
|
|||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
[GtkTemplate (ui = "/org/dino-im/conversation_titlebar.ui")]
|
||||
public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar {
|
||||
|
||||
[GtkChild]
|
||||
private MenuButton menu_button;
|
||||
|
||||
[GtkChild]
|
||||
private MenuButton encryption_button;
|
||||
private RadioButton? button_unencrypted;
|
||||
private RadioButton? button_pgp;
|
||||
|
||||
[GtkChild]
|
||||
private MenuButton groupchat_button;
|
||||
|
||||
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() {
|
||||
string? pgp_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart);
|
||||
button_pgp.set_sensitive(pgp_id != null);
|
||||
switch (conversation.encryption) {
|
||||
case Conversation.ENCRYPTION_UNENCRYPTED:
|
||||
button_unencrypted.set_active(true);
|
||||
break;
|
||||
case Conversation.ENCRYPTION_PGP:
|
||||
button_pgp.set_active(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void update_encryption_menu_icon() {
|
||||
encryption_button.visible = conversation.type_ == Conversation.TYPE_CHAT;
|
||||
if (conversation.type_ == Conversation.TYPE_CHAT) {
|
||||
if (conversation.encryption == Conversation.ENCRYPTION_UNENCRYPTED) {
|
||||
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 create_encryption_menu() {
|
||||
Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui");
|
||||
PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu;
|
||||
button_unencrypted = builder.get_object("button_unencrypted") as RadioButton;
|
||||
button_pgp = builder.get_object("button_pgp") as RadioButton;
|
||||
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));
|
||||
|
||||
button_unencrypted.toggled.connect(() => {
|
||||
if (conversation != null) {
|
||||
if (button_unencrypted.get_active()) {
|
||||
conversation.encryption = Conversation.ENCRYPTION_UNENCRYPTED;
|
||||
} else if (button_pgp.get_active()) {
|
||||
conversation.encryption = Conversation.ENCRYPTION_PGP;
|
||||
}
|
||||
update_encryption_menu_icon();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
client/src/ui/manage_accounts/account_row.vala
Normal file
|
@ -0,0 +1,24 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
}
|
70
client/src/ui/manage_accounts/add_account_dialog.vala
Normal file
|
@ -0,0 +1,70 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
221
client/src/ui/manage_accounts/dialog.vala
Normal file
|
@ -0,0 +1,221 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
|
||||
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] 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;
|
||||
|
||||
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_press_event.connect(on_password_entry_key_press_event);
|
||||
alias_entry.key_press_event.connect(on_alias_entry_key_press_event);
|
||||
image_button.clicked.connect(on_image_button_clicked);
|
||||
|
||||
main_stack.set_visible_child_name("no_accounts");
|
||||
}
|
||||
|
||||
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_button.clicked.connect(() => {
|
||||
password_stack.set_visible_child_name("entry");
|
||||
alias_stack.set_visible_child_name("label");
|
||||
set_focus(password_entry);
|
||||
});
|
||||
password_entry.text = account.password;
|
||||
|
||||
alias_label.label = account.alias;
|
||||
alias_stack.set_visible_child_name("label");
|
||||
alias_button.clicked.connect(() => {
|
||||
alias_stack.set_visible_child_name("entry");
|
||||
password_stack.set_visible_child_name("label");
|
||||
set_focus(alias_entry);
|
||||
});
|
||||
alias_entry.text = account.alias;
|
||||
|
||||
active_switch.state_set.connect(on_active_switch_state_changed);
|
||||
}
|
||||
|
||||
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_entry_key_press_event(EventKey event) {
|
||||
Account account = (account_list.get_selected_row() as AccountRow).account;
|
||||
if (event.keyval == Key.Return) {
|
||||
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;
|
||||
password_stack.set_visible_child_name("label");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool on_alias_entry_key_press_event(EventKey event) {
|
||||
Account account = (account_list.get_selected_row() as AccountRow).account;
|
||||
if (event.keyval == Key.Return) {
|
||||
account.alias = alias_entry.text;
|
||||
alias_label.label = alias_entry.text;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
client/src/ui/notifications.vala
Normal file
|
@ -0,0 +1,55 @@
|
|||
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) { }
|
||||
}
|
||||
}
|
||||
}
|
112
client/src/ui/occupant_list.vala
Normal file
|
@ -0,0 +1,112 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
27
client/src/ui/occupant_list_row.vala
Normal file
|
@ -0,0 +1,27 @@
|
|||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
27
client/src/ui/settings_dialog.vala
Normal file
|
@ -0,0 +1,27 @@
|
|||
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; });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
client/src/ui/unified_window.vala
Normal file
|
@ -0,0 +1,78 @@
|
|||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
public class Dino.Ui.UnifiedWindow : ApplicationWindow {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
71
client/src/ui/util.vala
Normal file
|
@ -0,0 +1,71 @@
|
|||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
using Xmpp;
|
||||
|
||||
public class Dino.Ui.Util : GLib.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()));
|
||||
}
|
||||
}
|
57
cmake/BuildTargetScript.cmake
Normal file
|
@ -0,0 +1,57 @@
|
|||
# This file is used to be invoked at build time. It generates the needed
|
||||
# resource XML file.
|
||||
|
||||
# Input variables that need to provided when invoking this script:
|
||||
# GXML_OUTPUT The output file path where to save the XML file.
|
||||
# GXML_COMPRESS_ALL Sets all COMPRESS flags in all resources in resource
|
||||
# list.
|
||||
# GXML_NO_COMPRESS_ALL Removes all COMPRESS flags in all resources in
|
||||
# resource list.
|
||||
# GXML_STRIPBLANKS_ALL Sets all STRIPBLANKS flags in all resources in
|
||||
# resource list.
|
||||
# GXML_NO_STRIPBLANKS_ALL Removes all STRIPBLANKS flags in all resources in
|
||||
# resource list.
|
||||
# GXML_TOPIXDATA_ALL Sets all TOPIXDATA flags i nall resources in resource
|
||||
# list.
|
||||
# GXML_NO_TOPIXDATA_ALL Removes all TOPIXDATA flags in all resources in
|
||||
# resource list.
|
||||
# GXML_PREFIX Overrides the resource prefix that is prepended to
|
||||
# each relative name in registered resources.
|
||||
# GXML_RESOURCES The list of resource files. Whether absolute or
|
||||
# relative path is equal.
|
||||
|
||||
# Include the GENERATE_GXML() function.
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/GenerateGXML.cmake)
|
||||
|
||||
# Set flags to actual invocation flags.
|
||||
if(GXML_COMPRESS_ALL)
|
||||
set(GXML_COMPRESS_ALL COMPRESS_ALL)
|
||||
endif()
|
||||
if(GXML_NO_COMPRESS_ALL)
|
||||
set(GXML_NO_COMPRESS_ALL NO_COMPRESS_ALL)
|
||||
endif()
|
||||
if(GXML_STRIPBLANKS_ALL)
|
||||
set(GXML_STRIPBLANKS_ALL STRIPBLANKS_ALL)
|
||||
endif()
|
||||
if(GXML_NO_STRIPBLANKS_ALL)
|
||||
set(GXML_NO_STRIPBLANKS_ALL NO_STRIPBLANKS_ALL)
|
||||
endif()
|
||||
if(GXML_TOPIXDATA_ALL)
|
||||
set(GXML_TOPIXDATA_ALL TOPIXDATA_ALL)
|
||||
endif()
|
||||
if(GXML_NO_TOPIXDATA_ALL)
|
||||
set(GXML_NO_TOPIXDATA_ALL NO_TOPIXDATA_ALL)
|
||||
endif()
|
||||
|
||||
# Replace " " with ";" to import the list over the command line. Otherwise
|
||||
# CMake would interprete the passed resources as a whole string.
|
||||
string(REPLACE " " ";" GXML_RESOURCES ${GXML_RESOURCES})
|
||||
|
||||
# Invoke the gresource XML generation function.
|
||||
generate_gxml(${GXML_OUTPUT}
|
||||
${GXML_COMPRESS_ALL} ${GXML_NO_COMPRESS_ALL}
|
||||
${GXML_STRIPBLANKS_ALL} ${GXML_NO_STRIPBLANKS_ALL}
|
||||
${GXML_TOPIXDATA_ALL} ${GXML_NO_TOPIXDATA_ALL}
|
||||
PREFIX ${GXML_PREFIX}
|
||||
RESOURCES ${GXML_RESOURCES})
|
||||
|
221
cmake/CompileGResources.cmake
Normal file
|
@ -0,0 +1,221 @@
|
|||
include(CMakeParseArguments)
|
||||
|
||||
# Path to this file.
|
||||
set(GCR_CMAKE_MACRO_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
# Compiles a gresource resource file from given resource files. Automatically
|
||||
# creates the XML controlling file.
|
||||
# The type of resource to generate (header, c-file or bundle) is automatically
|
||||
# determined from TARGET file ending, if no TYPE is explicitly specified.
|
||||
# The output file is stored in the provided variable "output".
|
||||
# "xml_out" contains the variable where to output the XML path. Can be used to
|
||||
# create custom targets or doing postprocessing.
|
||||
# If you want to use preprocessing, you need to manually check the existence
|
||||
# of the tools you use. This function doesn't check this for you, it just
|
||||
# generates the XML file. glib-compile-resources will then throw a
|
||||
# warning/error.
|
||||
function(COMPILE_GRESOURCES output xml_out)
|
||||
# Available options:
|
||||
# COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all
|
||||
# registered resources.
|
||||
# STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all
|
||||
# registered resources.
|
||||
# TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all
|
||||
# registered resources.
|
||||
set(CG_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL
|
||||
STRIPBLANKS_ALL NO_STRIPBLANKS_ALL
|
||||
TOPIXDATA_ALL NO_TOPIXDATA_ALL)
|
||||
|
||||
# Available one value options:
|
||||
# TYPE Type of resource to create. Valid options are:
|
||||
# EMBED_C: A C-file that can be compiled with your project.
|
||||
# EMBED_H: A header that can be included into your project.
|
||||
# BUNDLE: Generates a resource bundle file that can be loaded
|
||||
# at runtime.
|
||||
# AUTO: Determine from target file ending. Need to specify
|
||||
# target argument.
|
||||
# PREFIX Overrides the resource prefix that is prepended to each
|
||||
# relative file name in registered resources.
|
||||
# SOURCE_DIR Overrides the resources base directory to search for resources.
|
||||
# Normally this is set to the source directory with that CMake
|
||||
# was invoked (CMAKE_SOURCE_DIR).
|
||||
# TARGET Overrides the name of the output file/-s. Normally the output
|
||||
# names from glib-compile-resources tool is taken.
|
||||
set(CG_ONEVALUEARGS TYPE PREFIX SOURCE_DIR TARGET)
|
||||
|
||||
# Available multi-value options:
|
||||
# RESOURCES The list of resource files. Whether absolute or relative path is
|
||||
# equal, absolute paths are stripped down to relative ones. If the
|
||||
# absolute path is not inside the given base directory SOURCE_DIR
|
||||
# or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this
|
||||
# function aborts.
|
||||
# OPTIONS Extra command line options passed to glib-compile-resources.
|
||||
set(CG_MULTIVALUEARGS RESOURCES OPTIONS)
|
||||
|
||||
# Parse the arguments.
|
||||
cmake_parse_arguments(CG_ARG
|
||||
"${CG_OPTIONS}"
|
||||
"${CG_ONEVALUEARGS}"
|
||||
"${CG_MULTIVALUEARGS}"
|
||||
"${ARGN}")
|
||||
|
||||
# Variable to store the double-quote (") string. Since escaping
|
||||
# double-quotes in strings is not possible we need a helper variable that
|
||||
# does this job for us.
|
||||
set(Q \")
|
||||
|
||||
# Check invocation validity with the <prefix>_UNPARSED_ARGUMENTS variable.
|
||||
# If other not recognized parameters were passed, throw error.
|
||||
if (CG_ARG_UNPARSED_ARGUMENTS)
|
||||
set(CG_WARNMSG "Invocation of COMPILE_GRESOURCES with unrecognized")
|
||||
set(CG_WARNMSG "${CG_WARNMSG} parameters. Parameters are:")
|
||||
set(CG_WARNMSG "${CG_WARNMSG} ${CG_ARG_UNPARSED_ARGUMENTS}.")
|
||||
message(WARNING ${CG_WARNMSG})
|
||||
endif()
|
||||
|
||||
# Check invocation validity depending on generation mode (EMBED_C, EMBED_H
|
||||
# or BUNDLE).
|
||||
if ("${CG_ARG_TYPE}" STREQUAL "EMBED_C")
|
||||
# EMBED_C mode, output compilable C-file.
|
||||
set(CG_GENERATE_COMMAND_LINE "--generate-source")
|
||||
set(CG_TARGET_FILE_ENDING "c")
|
||||
elseif ("${CG_ARG_TYPE}" STREQUAL "EMBED_H")
|
||||
# EMBED_H mode, output includable header file.
|
||||
set(CG_GENERATE_COMMAND_LINE "--generate-header")
|
||||
set(CG_TARGET_FILE_ENDING "h")
|
||||
elseif ("${CG_ARG_TYPE}" STREQUAL "BUNDLE")
|
||||
# BUNDLE mode, output resource bundle. Don't do anything since
|
||||
# glib-compile-resources outputs a bundle when not specifying
|
||||
# something else.
|
||||
set(CG_TARGET_FILE_ENDING "gresource")
|
||||
else()
|
||||
# Everything else is AUTO mode, determine from target file ending.
|
||||
if (CG_ARG_TARGET)
|
||||
set(CG_GENERATE_COMMAND_LINE "--generate")
|
||||
else()
|
||||
set(CG_ERRMSG "AUTO mode given, but no target specified. Can't")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} determine output type. In function")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${CG_ERRMSG})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Check flag validity.
|
||||
if (CG_ARG_COMPRESS_ALL AND CG_ARG_NO_COMPRESS_ALL)
|
||||
set(CG_ERRMSG "COMPRESS_ALL and NO_COMPRESS_ALL simultaneously set. In")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} function COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${CG_ERRMSG})
|
||||
endif()
|
||||
if (CG_ARG_STRIPBLANKS_ALL AND CG_ARG_NO_STRIPBLANKS_ALL)
|
||||
set(CG_ERRMSG "STRIPBLANKS_ALL and NO_STRIPBLANKS_ALL simultaneously")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} set. In function COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${CG_ERRMSG})
|
||||
endif()
|
||||
if (CG_ARG_TOPIXDATA_ALL AND CG_ARG_NO_TOPIXDATA_ALL)
|
||||
set(CG_ERRMSG "TOPIXDATA_ALL and NO_TOPIXDATA_ALL simultaneously set.")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} In function COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${CG_ERRMSG})
|
||||
endif()
|
||||
|
||||
# Check if there are any resources.
|
||||
if (NOT CG_ARG_RESOURCES)
|
||||
set(CG_ERRMSG "No resource files to process. In function")
|
||||
set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${CG_ERRMSG})
|
||||
endif()
|
||||
|
||||
# Extract all dependencies for targets from resource list.
|
||||
foreach(res ${CG_ARG_RESOURCES})
|
||||
if (NOT(("${res}" STREQUAL "COMPRESS") OR
|
||||
("${res}" STREQUAL "STRIPBLANKS") OR
|
||||
("${res}" STREQUAL "TOPIXDATA")))
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_BINARY_DIR}/resources/${res}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CG_ARG_SOURCE_DIR}/${res}" "${CMAKE_BINARY_DIR}/resources/${res}"
|
||||
MAIN_DEPENDENCY "${CG_ARG_SOURCE_DIR}/${res}")
|
||||
list(APPEND CG_RESOURCES_DEPENDENCIES "${CMAKE_BINARY_DIR}/resources/${res}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
|
||||
# Construct .gresource.xml path.
|
||||
set(CG_XML_FILE_PATH "${CMAKE_BINARY_DIR}/resources/.gresource.xml")
|
||||
|
||||
# Generate gresources XML target.
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_OUTPUT=${Q}${CG_XML_FILE_PATH}${Q}")
|
||||
if(CG_ARG_COMPRESS_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_COMPRESS_ALL")
|
||||
endif()
|
||||
if(CG_ARG_NO_COMPRESS_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_COMPRESS_ALL")
|
||||
endif()
|
||||
if(CG_ARG_STRPIBLANKS_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_STRIPBLANKS_ALL")
|
||||
endif()
|
||||
if(CG_ARG_NO_STRIPBLANKS_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_STRIPBLANKS_ALL")
|
||||
endif()
|
||||
if(CG_ARG_TOPIXDATA_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_TOPIXDATA_ALL")
|
||||
endif()
|
||||
if(CG_ARG_NO_TOPIXDATA_ALL)
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_TOPIXDATA_ALL")
|
||||
endif()
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_PREFIX=${Q}${CG_ARG_PREFIX}${Q}")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS
|
||||
"GXML_RESOURCES=${Q}${CG_ARG_RESOURCES}${Q}")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS "-P")
|
||||
list(APPEND CG_CMAKE_SCRIPT_ARGS
|
||||
"${Q}${GCR_CMAKE_MACRO_DIR}/BuildTargetScript.cmake${Q}")
|
||||
|
||||
get_filename_component(CG_XML_FILE_PATH_ONLY_NAME
|
||||
"${CG_XML_FILE_PATH}" NAME)
|
||||
set(CG_XML_CUSTOM_COMMAND_COMMENT
|
||||
"Creating gresources XML file (${CG_XML_FILE_PATH_ONLY_NAME})")
|
||||
add_custom_command(OUTPUT ${CG_XML_FILE_PATH}
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
ARGS ${CG_CMAKE_SCRIPT_ARGS}
|
||||
DEPENDS ${CG_RESOURCES_DEPENDENCIES}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT ${CG_XML_CUSTOM_COMMAND_COMMENT})
|
||||
|
||||
# Create target manually if not set (to make sure glib-compile-resources
|
||||
# doesn't change behaviour with it's naming standards).
|
||||
if (NOT CG_ARG_TARGET)
|
||||
set(CG_ARG_TARGET "${CMAKE_BINARY_DIR}/resources")
|
||||
set(CG_ARG_TARGET "${CG_ARG_TARGET}.${CG_TARGET_FILE_ENDING}")
|
||||
endif()
|
||||
|
||||
# Create source directory automatically if not set.
|
||||
if (NOT CG_ARG_SOURCE_DIR)
|
||||
set(CG_ARG_SOURCE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
endif()
|
||||
|
||||
# Add compilation target for resources.
|
||||
add_custom_command(OUTPUT ${CG_ARG_TARGET}
|
||||
COMMAND ${GLIB_COMPILE_RESOURCES_EXECUTABLE}
|
||||
ARGS
|
||||
${OPTIONS}
|
||||
"--target=${Q}${CG_ARG_TARGET}${Q}"
|
||||
"--sourcedir=${Q}${CG_ARG_SOURCE_DIR}${Q}"
|
||||
${CG_GENERATE_COMMAND_LINE}
|
||||
${CG_XML_FILE_PATH}
|
||||
MAIN_DEPENDENCY ${CG_XML_FILE_PATH}
|
||||
DEPENDS ${CG_RESOURCES_DEPENDENCIES}
|
||||
WORKING_DIRECTORY ${CMAKE_BUILD_DIR})
|
||||
|
||||
# Set output and XML_OUT to parent scope.
|
||||
set(${xml_out} ${CG_XML_FILE_PATH} PARENT_SCOPE)
|
||||
set(${output} ${CG_ARG_TARGET} PARENT_SCOPE)
|
||||
|
||||
endfunction()
|
27
cmake/FindGPGME.cmake
Normal file
|
@ -0,0 +1,27 @@
|
|||
# TODO: Windows related stuff
|
||||
|
||||
find_program(GPGME_CONFIG_EXECUTABLE NAMES gpgme-config)
|
||||
mark_as_advanced(GPGME_CONFIG_EXECUTABLE)
|
||||
|
||||
if(GPGME_CONFIG_EXECUTABLE)
|
||||
execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --version
|
||||
OUTPUT_VARIABLE GPGME_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --api-version
|
||||
OUTPUT_VARIABLE GPGME_API_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --cflags
|
||||
OUTPUT_VARIABLE GPGME_CFLAGS
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
execute_process(COMMAND ${GPGME_CONFIG_EXECUTABLE} --libs
|
||||
OUTPUT_VARIABLE GPGME_LIBRARIES
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
endif(GPGME_CONFIG_EXECUTABLE)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(GPGME
|
||||
REQUIRED_VARS GPGME_CONFIG_EXECUTABLE
|
||||
VERSION_VAR GPGME_VERSION)
|
42
cmake/FindLIBUUID.cmake
Normal file
|
@ -0,0 +1,42 @@
|
|||
# - Find libuuid
|
||||
# Find the libuuid library
|
||||
#
|
||||
# This module defines the following variables:
|
||||
# LIBUUID_FOUND - True if library and include directory are found
|
||||
# If set to TRUE, the following are also defined:
|
||||
# LIBUUID_INCLUDE_DIRS - The directory where to find the header file
|
||||
# LIBUUID_LIBRARIES - Where to find the library file
|
||||
#
|
||||
# For conveniance, these variables are also set. They have the same values
|
||||
# than the variables above. The user can thus choose his/her prefered way
|
||||
# to write them.
|
||||
# LIBUUID_INCLUDE_DIR
|
||||
# LIBUUID_LIBRARY
|
||||
#
|
||||
# This file is in the public domain
|
||||
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(LIBUUID uuid)
|
||||
|
||||
if(NOT LIBUUID_FOUND)
|
||||
find_path(LIBUUID_INCLUDE_DIRS NAMES uuid/uuid.h
|
||||
PATH_SUFFIXES uuid
|
||||
DOC "The libuuid include directory")
|
||||
|
||||
find_library(LIBUUID_LIBRARIES NAMES uuid
|
||||
DOC "The libuuid library")
|
||||
|
||||
# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
|
||||
# set LIBUUID_FOUND to TRUE if these two variables are set.
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBUUID REQUIRED_VARS LIBUUID_LIBRARIES LIBUUID_INCLUDE_DIRS)
|
||||
|
||||
# Compatibility for all the ways of writing these variables
|
||||
if(LIBUUID_FOUND)
|
||||
set(LIBUUID_INCLUDE_DIR ${LIBUUID_INCLUDE_DIRS})
|
||||
set(LIBUUID_LIBRARY ${LIBUUID_LIBRARIES})
|
||||
set(LIBUUID_CFLAGS -I${LIBUUID_INCLUDE_DIRS})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(LIBUUID_INCLUDE_DIRS LIBUUID_LIBRARIES LIBUUID_CFLAGS)
|
70
cmake/FindVala.cmake
Normal file
|
@ -0,0 +1,70 @@
|
|||
##
|
||||
# Find module for the Vala compiler (valac)
|
||||
#
|
||||
# This module determines wheter a Vala compiler is installed on the current
|
||||
# system and where its executable is.
|
||||
#
|
||||
# Call the module using "find_package(Vala) from within your CMakeLists.txt.
|
||||
#
|
||||
# The following variables will be set after an invocation:
|
||||
#
|
||||
# VALA_FOUND Whether the vala compiler has been found or not
|
||||
# VALA_EXECUTABLE Full path to the valac executable if it has been found
|
||||
# VALA_VERSION Version number of the available valac
|
||||
# VALA_USE_FILE Include this file to define the vala_precompile function
|
||||
##
|
||||
|
||||
##
|
||||
# Copyright 2009-2010 Jakob Westhoff. All rights reserved.
|
||||
# Copyright 2010-2011 Daniel Pfeifer
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY JAKOB WESTHOFF ``AS IS'' AND ANY EXPRESS OR
|
||||
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
# EVENT SHALL JAKOB WESTHOFF OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
# The views and conclusions contained in the software and documentation are those
|
||||
# of the authors and should not be interpreted as representing official policies,
|
||||
# either expressed or implied, of Jakob Westhoff
|
||||
##
|
||||
|
||||
# Search for the valac executable in the usual system paths
|
||||
# Some distributions rename the valac to contain the major.minor in the binary name
|
||||
find_program(VALA_EXECUTABLE NAMES valac valac-0.20 valac-0.18 valac-0.16 valac-0.14 valac-0.12 valac-0.10)
|
||||
mark_as_advanced(VALA_EXECUTABLE)
|
||||
|
||||
# Determine the valac version
|
||||
if(VALA_EXECUTABLE)
|
||||
execute_process(COMMAND ${VALA_EXECUTABLE} "--version"
|
||||
OUTPUT_VARIABLE VALA_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
string(REPLACE "Vala " "" VALA_VERSION "${VALA_VERSION}")
|
||||
endif(VALA_EXECUTABLE)
|
||||
|
||||
# Handle the QUIETLY and REQUIRED arguments, which may be given to the find call.
|
||||
# Furthermore set VALA_FOUND to TRUE if Vala has been found (aka.
|
||||
# VALA_EXECUTABLE is set)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Vala
|
||||
REQUIRED_VARS VALA_EXECUTABLE
|
||||
VERSION_VAR VALA_VERSION)
|
||||
|
||||
set(VALA_USE_FILE "${CMAKE_CURRENT_LIST_DIR}/UseVala.cmake")
|
||||
|
124
cmake/GenerateGXML.cmake
Normal file
|
@ -0,0 +1,124 @@
|
|||
include(CMakeParseArguments)
|
||||
|
||||
# Generates the resource XML controlling file from resource list (and saves it
|
||||
# to xml_path). It's not recommended to use this function directly, since it
|
||||
# doesn't handle invalid arguments. It is used by the function
|
||||
# COMPILE_GRESOURCES() to create a custom command, so that this function is
|
||||
# invoked at build-time in script mode from CMake.
|
||||
function(GENERATE_GXML xml_path)
|
||||
# Available options:
|
||||
# COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all
|
||||
# registered resources.
|
||||
# STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all
|
||||
# registered resources.
|
||||
# TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all
|
||||
# registered resources.
|
||||
set(GXML_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL
|
||||
STRIPBLANKS_ALL NO_STRIPBLANKS_ALL
|
||||
TOPIXDATA_ALL NO_TOPIXDATA_ALL)
|
||||
|
||||
# Available one value options:
|
||||
# PREFIX Overrides the resource prefix that is prepended to each
|
||||
# relative file name in registered resources.
|
||||
set(GXML_ONEVALUEARGS PREFIX)
|
||||
|
||||
# Available multi-value options:
|
||||
# RESOURCES The list of resource files. Whether absolute or relative path is
|
||||
# equal, absolute paths are stripped down to relative ones. If the
|
||||
# absolute path is not inside the given base directory SOURCE_DIR
|
||||
# or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this
|
||||
# function aborts.
|
||||
set(GXML_MULTIVALUEARGS RESOURCES)
|
||||
|
||||
# Parse the arguments.
|
||||
cmake_parse_arguments(GXML_ARG
|
||||
"${GXML_OPTIONS}"
|
||||
"${GXML_ONEVALUEARGS}"
|
||||
"${GXML_MULTIVALUEARGS}"
|
||||
"${ARGN}")
|
||||
|
||||
# Variable to store the double-quote (") string. Since escaping
|
||||
# double-quotes in strings is not possible we need a helper variable that
|
||||
# does this job for us.
|
||||
set(Q \")
|
||||
|
||||
# Process resources and generate XML file.
|
||||
# Begin with the XML header and header nodes.
|
||||
set(GXML_XML_FILE "<?xml version=${Q}1.0${Q} encoding=${Q}UTF-8${Q}?>")
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}<gresources><gresource prefix=${Q}")
|
||||
|
||||
# Set the prefix for the resources. Depending on the user-override we choose
|
||||
# the standard prefix "/" or the override.
|
||||
if (GXML_ARG_PREFIX)
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_ARG_PREFIX}")
|
||||
else()
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}/")
|
||||
endif()
|
||||
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}${Q}>")
|
||||
|
||||
# Process each resource.
|
||||
foreach(res ${GXML_ARG_RESOURCES})
|
||||
if ("${res}" STREQUAL "COMPRESS")
|
||||
set(GXML_COMPRESSION_FLAG ON)
|
||||
elseif ("${res}" STREQUAL "STRIPBLANKS")
|
||||
set(GXML_STRIPBLANKS_FLAG ON)
|
||||
elseif ("${res}" STREQUAL "TOPIXDATA")
|
||||
set(GXML_TOPIXDATA_FLAG ON)
|
||||
else()
|
||||
# The file name.
|
||||
set(GXML_RESOURCE_PATH "${res}")
|
||||
|
||||
# Append to real resource file dependency list.
|
||||
list(APPEND GXML_RESOURCES_DEPENDENCIES ${GXML_RESOURCE_PATH})
|
||||
|
||||
# Assemble <file> node.
|
||||
set(GXML_RES_LINE "<file")
|
||||
if ((GXML_ARG_COMPRESS_ALL OR GXML_COMPRESSION_FLAG) AND NOT
|
||||
GXML_ARG_NO_COMPRESS_ALL)
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE} compressed=${Q}true${Q}")
|
||||
endif()
|
||||
|
||||
# Check preprocess flag validity.
|
||||
if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND
|
||||
(GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG))
|
||||
set(GXML_ERRMSG "Resource preprocessing option conflict. Tried")
|
||||
set(GXML_ERRMSG "${GXML_ERRMSG} to specify both, STRIPBLANKS")
|
||||
set(GXML_ERRMSG "${GXML_ERRMSG} and TOPIXDATA. In resource")
|
||||
set(GXML_ERRMSG "${GXML_ERRMSG} ${GXML_RESOURCE_PATH} in")
|
||||
set(GXML_ERRMSG "${GXML_ERRMSG} function COMPILE_GRESOURCES.")
|
||||
message(FATAL_ERROR ${GXML_ERRMSG})
|
||||
endif()
|
||||
|
||||
if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND NOT
|
||||
GXML_ARG_NO_STRIPBLANKS_ALL)
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE}${Q}xml-stripblanks${Q}")
|
||||
elseif((GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG) AND NOT
|
||||
GXML_ARG_NO_TOPIXDATA_ALL)
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE}${Q}to-pixdata${Q}")
|
||||
endif()
|
||||
|
||||
set(GXML_RES_LINE "${GXML_RES_LINE}>${GXML_RESOURCE_PATH}</file>")
|
||||
|
||||
# Append to file string.
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_RES_LINE}")
|
||||
|
||||
# Unset variables.
|
||||
unset(GXML_COMPRESSION_FLAG)
|
||||
unset(GXML_STRIPBLANKS_FLAG)
|
||||
unset(GXML_TOPIXDATA_FLAG)
|
||||
endif()
|
||||
|
||||
endforeach()
|
||||
|
||||
# Append closing nodes.
|
||||
set(GXML_XML_FILE "${GXML_XML_FILE}</gresource></gresources>")
|
||||
|
||||
# Use "file" function to generate XML controlling file.
|
||||
get_filename_component(xml_path_only_name "${xml_path}" NAME)
|
||||
file(WRITE ${xml_path} ${GXML_XML_FILE})
|
||||
|
||||
endfunction()
|
||||
|