aboutsummaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt22
-rw-r--r--main/data/call_widget.ui111
-rw-r--r--main/data/icons/dino-microphone-off-symbolic.svg1
-rw-r--r--main/data/icons/dino-microphone-symbolic.svg1
-rw-r--r--main/data/icons/dino-phone-hangup-symbolic.svg1
-rw-r--r--main/data/icons/dino-phone-in-talk-symbolic.svg1
-rw-r--r--main/data/icons/dino-phone-missed-symbolic.svg1
-rw-r--r--main/data/icons/dino-phone-ring-symbolic.svg1
-rw-r--r--main/data/icons/dino-phone-symbolic.svg1
-rw-r--r--main/data/icons/dino-video-off-symbolic.svg1
-rw-r--r--main/data/icons/dino-video-symbolic.svg1
-rw-r--r--main/data/theme.css114
-rw-r--r--main/src/main.vala2
-rw-r--r--main/src/ui/application.vala18
-rw-r--r--main/src/ui/call_window/audio_settings_popover.vala127
-rw-r--r--main/src/ui/call_window/call_bottom_bar.vala164
-rw-r--r--main/src/ui/call_window/call_encryption_button.vala77
-rw-r--r--main/src/ui/call_window/call_window.vala260
-rw-r--r--main/src/ui/call_window/call_window_controller.vala254
-rw-r--r--main/src/ui/call_window/video_settings_popover.vala73
-rw-r--r--main/src/ui/conversation_content_view/call_widget.vala215
-rw-r--r--main/src/ui/conversation_content_view/content_populator.vala4
-rw-r--r--main/src/ui/conversation_content_view/conversation_item_skeleton.vala74
-rw-r--r--main/src/ui/conversation_content_view/file_widget.vala3
-rw-r--r--main/src/ui/conversation_selector/conversation_selector_row.vala8
-rw-r--r--main/src/ui/conversation_titlebar/call_entry.vala132
-rw-r--r--main/src/ui/conversation_view_controller.vala1
-rw-r--r--main/src/ui/notifier_freedesktop.vala38
-rw-r--r--main/src/ui/notifier_gnotifications.vala19
-rw-r--r--main/src/ui/util/helper.vala15
30 files changed, 1702 insertions, 38 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 5169e8ae..4891abb0 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -5,6 +5,8 @@ gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TAR
find_packages(MAIN_PACKAGES REQUIRED
Gee
+ Gst
+ GstVideo
GLib
GModule
GObject
@@ -21,7 +23,14 @@ set(RESOURCE_LIST
icons/dino-emoticon-symbolic.svg
icons/dino-qr-code-symbolic.svg
icons/dino-security-high-symbolic.svg
+ icons/dino-microphone-off-symbolic.svg
+ icons/dino-microphone-symbolic.svg
icons/dino-party-popper-symbolic.svg
+ icons/dino-phone-hangup-symbolic.svg
+ icons/dino-phone-in-talk-symbolic.svg
+ icons/dino-phone-missed-symbolic.svg
+ icons/dino-phone-ring-symbolic.svg
+ icons/dino-phone-symbolic.svg
icons/dino-status-away.svg
icons/dino-status-chat.svg
icons/dino-status-dnd.svg
@@ -29,6 +38,8 @@ set(RESOURCE_LIST
icons/im.dino.Dino.svg
icons/im.dino.Dino-symbolic.svg
icons/dino-tick-symbolic.svg
+ icons/dino-video-off-symbolic.svg
+ icons/dino-video-symbolic.svg
icons/dino-device-desktop-symbolic.svg
icons/dino-device-phone-symbolic.svg
@@ -46,6 +57,8 @@ set(RESOURCE_LIST
add_conversation/conference_details_fragment.ui
add_conversation/list_row.ui
add_conversation/select_jid_fragment.ui
+
+ call_widget.ui
chat_input.ui
contact_details_dialog.ui
conversation_list_titlebar.ui
@@ -124,6 +137,13 @@ SOURCES
src/ui/add_conversation/select_contact_dialog.vala
src/ui/add_conversation/select_jid_fragment.vala
+ src/ui/call_window/audio_settings_popover.vala
+ src/ui/call_window/call_bottom_bar.vala
+ src/ui/call_window/call_encryption_button.vala
+ src/ui/call_window/call_window.vala
+ src/ui/call_window/call_window_controller.vala
+ src/ui/call_window/video_settings_popover.vala
+
src/ui/chat_input/chat_input_controller.vala
src/ui/chat_input/chat_text_view.vala
src/ui/chat_input/edit_history.vala
@@ -142,6 +162,7 @@ SOURCES
src/ui/conversation_selector/conversation_selector_row.vala
src/ui/conversation_selector/conversation_selector.vala
+ src/ui/conversation_content_view/call_widget.vala
src/ui/conversation_content_view/chat_state_populator.vala
src/ui/conversation_content_view/content_populator.vala
src/ui/conversation_content_view/conversation_item_skeleton.vala
@@ -153,6 +174,7 @@ SOURCES
src/ui/conversation_content_view/message_widget.vala
src/ui/conversation_content_view/subscription_notification.vala
+ src/ui/conversation_titlebar/call_entry.vala
src/ui/conversation_titlebar/menu_entry.vala
src/ui/conversation_titlebar/occupants_entry.vala
src/ui/conversation_titlebar/search_entry.vala
diff --git a/main/data/call_widget.ui b/main/data/call_widget.ui
new file mode 100644
index 00000000..47fb0046
--- /dev/null
+++ b/main/data/call_widget.ui
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiCallWidget">
+ <property name="halign">start</property>
+ <style>
+ <class name="call-box-outer"/>
+ </style>
+ <child>
+ <object class="DinoUiSizingBin">
+ <property name="target-width">350</property>
+ <property name="max-width">350</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="halign">fill</property>
+ <property name="hexpand">true</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="spacing">10</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="call-box"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="icon-size">5</property>
+ <property name="opacity">0.7</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="title_label">
+ <property name="ellipsize">middle</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle_label">
+ <property name="xalign">0</property>
+ <property name="yalign">1</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="incoming_call_revealer">
+ <property name="transition-type">slide-down</property>
+ <property name="transition-duration">200</property>
+ <property name="reveal-child">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <style>
+ <class name="incoming-call-box"/>
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="halign">end</property>
+ <property name="orientation">horizontal</property>
+ <property name="spacing">5</property>
+ <property name="margin">10</property>
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="reject_call_button">
+ <property name="label" translatable="yes">Reject</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="destructive-action"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="accept_call_button">
+ <property name="label" translatable="yes">Accept</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/icons/dino-microphone-off-symbolic.svg b/main/data/icons/dino-microphone-off-symbolic.svg
new file mode 100644
index 00000000..7e5b853d
--- /dev/null
+++ b/main/data/icons/dino-microphone-off-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-microphone-symbolic.svg b/main/data/icons/dino-microphone-symbolic.svg
new file mode 100644
index 00000000..fbf0784a
--- /dev/null
+++ b/main/data/icons/dino-microphone-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-phone-hangup-symbolic.svg b/main/data/icons/dino-phone-hangup-symbolic.svg
new file mode 100644
index 00000000..ecd230ac
--- /dev/null
+++ b/main/data/icons/dino-phone-hangup-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,9C10.4,9 8.85,9.25 7.4,9.72V12.82C7.4,13.22 7.17,13.56 6.84,13.72C5.86,14.21 4.97,14.84 4.17,15.57C4,15.75 3.75,15.86 3.5,15.86C3.2,15.86 2.95,15.74 2.77,15.56L0.29,13.08C0.11,12.9 0,12.65 0,12.38C0,12.1 0.11,11.85 0.29,11.67C3.34,8.77 7.46,7 12,7C16.54,7 20.66,8.77 23.71,11.67C23.89,11.85 24,12.1 24,12.38C24,12.65 23.89,12.9 23.71,13.08L21.23,15.56C21.05,15.74 20.8,15.86 20.5,15.86C20.25,15.86 20,15.75 19.82,15.57C19.03,14.84 18.14,14.21 17.16,13.72C16.83,13.56 16.6,13.22 16.6,12.82V9.72C15.15,9.25 13.6,9 12,9Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-phone-in-talk-symbolic.svg b/main/data/icons/dino-phone-in-talk-symbolic.svg
new file mode 100644
index 00000000..351035da
--- /dev/null
+++ b/main/data/icons/dino-phone-in-talk-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M15,12H17A5,5 0 0,0 12,7V9A3,3 0 0,1 15,12M19,12H21C21,7 16.97,3 12,3V5C15.86,5 19,8.13 19,12M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-phone-missed-symbolic.svg b/main/data/icons/dino-phone-missed-symbolic.svg
new file mode 100644
index 00000000..228f073e
--- /dev/null
+++ b/main/data/icons/dino-phone-missed-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M23.71,16.67C20.66,13.77 16.54,12 12,12C7.46,12 3.34,13.77 0.29,16.67C0.11,16.85 0,17.1 0,17.38C0,17.65 0.11,17.9 0.29,18.08L2.77,20.56C2.95,20.74 3.2,20.86 3.5,20.86C3.75,20.86 4,20.75 4.18,20.57C4.97,19.83 5.86,19.21 6.84,18.72C7.17,18.56 7.4,18.22 7.4,17.82V14.72C8.85,14.25 10.39,14 12,14C13.6,14 15.15,14.25 16.6,14.72V17.82C16.6,18.22 16.83,18.56 17.16,18.72C18.14,19.21 19.03,19.83 19.82,20.57C20,20.75 20.25,20.86 20.5,20.86C20.8,20.86 21.05,20.74 21.23,20.56L23.71,18.08C23.89,17.9 24,17.65 24,17.38C24,17.1 23.89,16.85 23.71,16.67M6.5,5.5L12,11L19,4L18,3L12,9L7.5,4.5H11V3H5V9H6.5V5.5Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-phone-ring-symbolic.svg b/main/data/icons/dino-phone-ring-symbolic.svg
new file mode 100644
index 00000000..06b8dcbf
--- /dev/null
+++ b/main/data/icons/dino-phone-ring-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M23.71 16.67C20.66 13.78 16.54 12 12 12S3.34 13.78.29 16.67c-.18.18-.29.43-.29.71 0 .28.11.53.29.71l2.48 2.48c.18.18.43.29.71.29.27 0 .52-.11.7-.28.79-.74 1.69-1.36 2.66-1.85.33-.16.56-.5.56-.9v-3.1c1.45-.48 3-.73 4.6-.73s3.15.25 4.6.72v3.1c0 .39.23.74.56.9.98.49 1.87 1.12 2.66 1.85.18.18.43.28.7.28.28 0 .53-.11.71-.29l2.48-2.48c.18-.18.29-.43.29-.71a.99.99 0 0 0-.29-.7zM21.16 6.26l-1.41-1.41-3.56 3.55 1.41 1.41s3.45-3.52 3.56-3.55zM13 2h-2v5h2V2zM6.4 9.81L7.81 8.4 4.26 4.84 2.84 6.26c.11.03 3.56 3.55 3.56 3.55z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-phone-symbolic.svg b/main/data/icons/dino-phone-symbolic.svg
new file mode 100644
index 00000000..0020dddc
--- /dev/null
+++ b/main/data/icons/dino-phone-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-video-off-symbolic.svg b/main/data/icons/dino-video-off-symbolic.svg
new file mode 100644
index 00000000..d438e065
--- /dev/null
+++ b/main/data/icons/dino-video-off-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3.27,2L2,3.27L4.73,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16C16.2,18 16.39,17.92 16.54,17.82L19.73,21L21,19.73M21,6.5L17,10.5V7A1,1 0 0,0 16,6H9.82L21,17.18V6.5Z" /></svg> \ No newline at end of file
diff --git a/main/data/icons/dino-video-symbolic.svg b/main/data/icons/dino-video-symbolic.svg
new file mode 100644
index 00000000..60a1c742
--- /dev/null
+++ b/main/data/icons/dino-video-symbolic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" /></svg> \ No newline at end of file
diff --git a/main/data/theme.css b/main/data/theme.css
index 6bacee30..454bd2c1 100644
--- a/main/data/theme.css
+++ b/main/data/theme.css
@@ -86,16 +86,26 @@ window.dino-main .dino-conversation .message-box.edit-mode:hover {
background: alpha(@theme_selected_bg_color, 0.12);
}
-window.dino-main .file-box-outer {
+window.dino-main .file-box-outer,
+window.dino-main .call-box-outer {
background: @theme_base_color;
border-radius: 3px;
border: 1px solid alpha(@theme_fg_color, 0.1);
}
-window.dino-main .file-box {
+window.dino-main .file-box,
+window.dino-main .call-box {
margin: 12px 16px 12px 12px;
}
+window.dino-main .call-box-outer.incoming {
+ border-color: alpha(@theme_selected_bg_color, 0.3);
+}
+
+window.dino-main .incoming-call-box {
+ background: alpha(@theme_selected_bg_color, 0.1);
+}
+
window.dino-main .dino-sidebar > frame.collapsed {
border-bottom: 1px solid @borders;
}
@@ -204,3 +214,103 @@ box.dino-input-error label.input-status-highlight-once {
animation-iteration-count: 1;
animation-name: input-error-highlight;
}
+
+/* Call window */
+
+.dino-call-window .titlebar {
+ min-height: 0;
+}
+
+.dino-call-window headerbar, .call-button {
+ box-shadow: none;
+}
+
+.dino-call-window .titlebutton.close:hover {
+ background: rgba(255,255,255,0.15);
+ border-color: rgba(255,255,255,0);
+ box-shadow: none;
+}
+
+.dino-call-window button.call-button {
+ outline: 0;
+ border-radius: 1000px;
+}
+
+.dino-call-window button.white-button {
+ color: #1d1c1d;
+ background: rgba(255,255,255,0.85);
+ border: lightgrey;
+}
+.dino-call-window button.white-button:hover {
+ background: rgba(255,255,255,1);
+}
+
+.dino-call-window button.transparent-white-button {
+ color: white;
+ background: rgba(255,255,255,0.15);
+ border: none;
+}
+.dino-call-window button.transparent-white-button:hover {
+ background: rgba(255,255,255,0.25);
+}
+
+.dino-call-window button.call-mediadevice-settings-button {
+ border-radius: 1000px;
+ min-height: 0;
+ min-width: 0;
+ padding: 3px;
+ margin: 2px;
+ transition-duration: 0;
+}
+
+.dino-call-window button.call-mediadevice-settings-button:hover,
+.dino-call-window button.call-mediadevice-settings-button:checked { /* Effect that makes the button slightly larger on hover :) */
+ border-radius: 1000px;
+ min-height: 0;
+ min-width: 0;
+ padding: 5px;
+ margin: 0;
+}
+
+.dino-call-window .encryption-box {
+ color: rgba(255,255,255,0.7);
+ border-radius: 5px;
+ background: rgba(0,0,0,0.5);
+ padding: 0px;
+ border: none;
+ box-shadow: none;
+}
+
+.dino-call-window .encryption-box.unencrypted {
+ color: @error_color;
+}
+
+.dino-call-window .encryption-box:hover {
+ background: rgba(20,20,20,0.5);
+}
+
+.dino-call-window .call-header-bar {
+ background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0));
+ border: 0;
+ border-radius: 0;
+}
+
+.dino-call-window .call-header-bar,
+.dino-call-window .call-header-bar image {
+ color: #ededec;
+}
+
+.dino-call-window .call-bottom-bar {
+ background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3));
+ border: 0;
+}
+
+.dino-call-window .video-placeholder-box {
+ background-color: #212121;
+}
+
+.dino-call-window .text-no-controls {
+ background: white;
+ border-radius: 5px;
+ padding: 5px 10px;
+} \ No newline at end of file
diff --git a/main/src/main.vala b/main/src/main.vala
index 6274dcdd..afa1f52b 100644
--- a/main/src/main.vala
+++ b/main/src/main.vala
@@ -17,7 +17,7 @@ void main(string[] args) {
Gtk.init(ref args);
Dino.Ui.Application app = new Dino.Ui.Application() { search_path_generator=search_path_generator };
Plugins.Loader loader = new Plugins.Loader(app);
- loader.loadAll();
+ loader.load_all();
app.run(args);
loader.shutdown();
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala
index 358097e3..780c37fd 100644
--- a/main/src/ui/application.vala
+++ b/main/src/ui/application.vala
@@ -199,6 +199,24 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
dialog.present();
});
add_action(open_shortcuts_action);
+
+ SimpleAction accept_call_action = new SimpleAction("accept-call", VariantType.INT32);
+ accept_call_action.activate.connect((variant) => {
+ Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
+ stream_interactor.get_module(Calls.IDENTITY).accept_call(call);
+
+ var call_window = new CallWindow();
+ call_window.controller = new CallWindowController(call_window, call, stream_interactor);
+ call_window.present();
+ });
+ add_action(accept_call_action);
+
+ SimpleAction deny_call_action = new SimpleAction("deny-call", VariantType.INT32);
+ deny_call_action.activate.connect((variant) => {
+ Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
+ stream_interactor.get_module(Calls.IDENTITY).reject_call(call);
+ });
+ add_action(deny_call_action);
}
public bool use_csd() {
diff --git a/main/src/ui/call_window/audio_settings_popover.vala b/main/src/ui/call_window/audio_settings_popover.vala
new file mode 100644
index 00000000..7d1f39b0
--- /dev/null
+++ b/main/src/ui/call_window/audio_settings_popover.vala
@@ -0,0 +1,127 @@
+using Gee;
+using Gtk;
+using Dino.Entities;
+
+public class Dino.Ui.AudioSettingsPopover : Gtk.Popover {
+
+ public signal void microphone_selected(Plugins.MediaDevice device);
+ public signal void speaker_selected(Plugins.MediaDevice device);
+
+ public Plugins.MediaDevice? current_microphone_device { get; set; }
+ public Plugins.MediaDevice? current_speaker_device { get; set; }
+
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_microphone_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_speaker_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+
+ public AudioSettingsPopover() {
+ Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
+ box.add(create_microphone_box());
+ box.add(create_speaker_box());
+
+ this.add(box);
+ }
+
+ private Widget create_microphone_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", false);
+
+ Box micro_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ micro_box.add(new Label("<b>" + _("Microphones") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
+
+ if (devices.size == 0) {
+ micro_box.add(new Label("No microphones found."));
+ } else {
+ ListBox micro_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ micro_list_box.set_header_func(listbox_header_func);
+ Frame micro_frame = new Frame(null) { visible=true };
+ micro_frame.add(micro_list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_microphone_device == null || current_microphone_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-microphone-device"].connect(() => {
+ if (current_microphone_device == null || current_microphone_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ micro_list_box.add(list_box_row);
+
+ row_microphone_device[list_box_row] = device;
+ }
+ micro_list_box.row_activated.connect((row) => {
+ if (!row_microphone_device.has_key(row)) return;
+ microphone_selected(row_microphone_device[row]);
+ micro_list_box.unselect_row(row);
+ });
+ micro_box.add(micro_frame);
+ }
+
+ return micro_box;
+ }
+
+ private Widget create_speaker_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", true);
+
+ Box speaker_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ speaker_box.add(new Label("<b>" + _("Speakers") +"</b>") { use_markup=true, xalign=0, visible=true });
+
+ if (devices.size == 0) {
+ speaker_box.add(new Label("No speakers found."));
+ } else {
+ ListBox speaker_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ speaker_list_box.set_header_func(listbox_header_func);
+ speaker_list_box.row_selected.connect((row) => {
+
+ });
+ Frame speaker_frame = new Frame(null) { visible=true };
+ speaker_frame.add(speaker_list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_speaker_device == null || current_speaker_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-speaker-device"].connect(() => {
+ if (current_speaker_device == null || current_speaker_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ speaker_list_box.add(list_box_row);
+
+ row_speaker_device[list_box_row] = device;
+ }
+ speaker_list_box.row_activated.connect((row) => {
+ if (!row_speaker_device.has_key(row)) return;
+ speaker_selected(row_speaker_device[row]);
+ speaker_list_box.unselect_row(row);
+ });
+ speaker_box.add(speaker_frame);
+ }
+
+ return speaker_box;
+ }
+
+ private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala
new file mode 100644
index 00000000..8a0604b3
--- /dev/null
+++ b/main/src/ui/call_window/call_bottom_bar.vala
@@ -0,0 +1,164 @@
+using Dino.Entities;
+using Gtk;
+using Pango;
+
+public class Dino.Ui.CallBottomBar : Gtk.Box {
+
+ public signal void hang_up();
+
+ public bool audio_enabled { get; set; }
+ public bool video_enabled { get; set; }
+
+ public static IconSize ICON_SIZE_MEDIADEVICE_BUTTON = Gtk.icon_size_register("im.dino.Dino.CALL_MEDIADEVICE_BUTTON", 10, 10);
+
+ public string counterpart_display_name { get; set; }
+
+ private Button audio_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ private Overlay audio_button_overlay = new Overlay() { visible=true };
+ private Image audio_image = new Image() { visible=true };
+ private MenuButton audio_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
+ public AudioSettingsPopover? audio_settings_popover;
+
+ private Button video_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ private Overlay video_button_overlay = new Overlay() { visible=true };
+ private Image video_image = new Image() { visible=true };
+ private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
+ public VideoSettingsPopover? video_settings_popover;
+
+ public CallEntryptionButton encryption_button = new CallEntryptionButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END };
+
+ private Label label = new Label("") { margin=20, halign=Align.CENTER, valign=Align.CENTER, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, visible=true };
+ private Stack stack = new Stack() { visible=true };
+
+ public CallBottomBar() {
+ Object(orientation:Orientation.HORIZONTAL, spacing:0);
+
+ Overlay default_control = new Overlay() { visible=true };
+ default_control.add_overlay(encryption_button);
+
+ Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true };
+
+ audio_button.add(audio_image);
+ audio_button.get_style_context().add_class("call-button");
+ audio_button.clicked.connect(() => { audio_enabled = !audio_enabled; });
+ audio_button.margin_end = audio_button.margin_bottom = 5; // space for the small settings button
+ audio_button_overlay.add(audio_button);
+ audio_button_overlay.add_overlay(audio_settings_button);
+ audio_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
+ audio_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
+ audio_settings_button.use_popover = true;
+ main_buttons.add(audio_button_overlay);
+
+ video_button.add(video_image);
+ video_button.get_style_context().add_class("call-button");
+ video_button.clicked.connect(() => { video_enabled = !video_enabled; });
+ video_button.margin_end = video_button.margin_bottom = 5;
+ video_button_overlay.add(video_button);
+ video_button_overlay.add_overlay(video_settings_button);
+ video_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
+ video_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
+ video_settings_button.use_popover = true;
+ main_buttons.add(video_button_overlay);
+
+ Button button_hang = new Button.from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR) { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
+ button_hang.get_style_context().add_class("call-button");
+ button_hang.get_style_context().add_class("destructive-action");
+ button_hang.clicked.connect(() => hang_up());
+ main_buttons.add(button_hang);
+
+ default_control.add(main_buttons);
+
+ label.get_style_context().add_class("text-no-controls");
+
+ stack.add_named(default_control, "control-buttons");
+ stack.add_named(label, "label");
+ this.add(stack);
+
+ this.notify["audio-enabled"].connect(on_audio_enabled_changed);
+ this.notify["video-enabled"].connect(on_video_enabled_changed);
+
+ audio_enabled = true;
+ video_enabled = false;
+
+ on_audio_enabled_changed();
+ on_video_enabled_changed();
+
+ this.get_style_context().add_class("call-bottom-bar");
+ }
+
+ public AudioSettingsPopover? show_audio_device_choices(bool show) {
+ audio_settings_button.visible = show;
+ if (audio_settings_popover != null) audio_settings_popover.visible = false;
+ if (!show) return null;
+
+ audio_settings_popover = new AudioSettingsPopover();
+
+ audio_settings_button.popover = audio_settings_popover;
+
+ audio_settings_popover.set_relative_to(audio_settings_button);
+ audio_settings_popover.microphone_selected.connect(() => { audio_settings_button.active = false; });
+ audio_settings_popover.speaker_selected.connect(() => { audio_settings_button.active = false; });
+
+ return audio_settings_popover;
+ }
+
+ public void show_audio_device_error() {
+ audio_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
+ Util.force_error_color(audio_settings_button);
+ }
+
+ public VideoSettingsPopover? show_video_device_choices(bool show) {
+ video_settings_button.visible = show;
+ if (video_settings_popover != null) video_settings_popover.visible = false;
+ if (!show) return null;
+
+ video_settings_popover = new VideoSettingsPopover();
+
+
+ video_settings_button.popover = video_settings_popover;
+
+ video_settings_popover.set_relative_to(video_settings_button);
+ video_settings_popover.camera_selected.connect(() => { video_settings_button.active = false; });
+
+ return video_settings_popover;
+ }
+
+ public void show_video_device_error() {
+ video_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
+ Util.force_error_color(video_settings_button);
+ }
+
+ public void on_audio_enabled_changed() {
+ if (audio_enabled) {
+ audio_image.set_from_icon_name("dino-microphone-symbolic", IconSize.LARGE_TOOLBAR);
+ audio_button.get_style_context().add_class("white-button");
+ audio_button.get_style_context().remove_class("transparent-white-button");
+ } else {
+ audio_image.set_from_icon_name("dino-microphone-off-symbolic", IconSize.LARGE_TOOLBAR);
+ audio_button.get_style_context().remove_class("white-button");
+ audio_button.get_style_context().add_class("transparent-white-button");
+ }
+ }
+
+ public void on_video_enabled_changed() {
+ if (video_enabled) {
+ video_image.set_from_icon_name("dino-video-symbolic", IconSize.LARGE_TOOLBAR);
+ video_button.get_style_context().add_class("white-button");
+ video_button.get_style_context().remove_class("transparent-white-button");
+
+ } else {
+ video_image.set_from_icon_name("dino-video-off-symbolic", IconSize.LARGE_TOOLBAR);
+ video_button.get_style_context().remove_class("white-button");
+ video_button.get_style_context().add_class("transparent-white-button");
+ }
+ }
+
+ public void show_counterpart_ended(string text) {
+ stack.set_visible_child_name("label");
+ label.label = text;
+ }
+
+ public bool is_menu_active() {
+ return video_settings_button.active || audio_settings_button.active || encryption_button.active;
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_encryption_button.vala b/main/src/ui/call_window/call_encryption_button.vala
new file mode 100644
index 00000000..1d785d51
--- /dev/null
+++ b/main/src/ui/call_window/call_encryption_button.vala
@@ -0,0 +1,77 @@
+using Dino.Entities;
+using Gtk;
+using Pango;
+
+public class Dino.Ui.CallEntryptionButton : MenuButton {
+
+ private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true };
+
+ construct {
+ add(encryption_image);
+ get_style_context().add_class("encryption-box");
+ this.set_popover(popover);
+ }
+
+ public void set_icon(bool encrypted, string? icon_name) {
+ this.visible = true;
+
+ if (encrypted) {
+ encryption_image.set_from_icon_name(icon_name ?? "changes-prevent-symbolic", IconSize.BUTTON);
+ get_style_context().remove_class("unencrypted");
+ } else {
+ encryption_image.set_from_icon_name(icon_name ?? "changes-allow-symbolic", IconSize.BUTTON);
+ get_style_context().add_class("unencrypted");
+ }
+ }
+
+ public void set_info(string? title, bool show_keys, Xmpp.Xep.Jingle.ContentEncryption? audio_encryption, Xmpp.Xep.Jingle.ContentEncryption? video_encryption) {
+ Popover popover = new Popover(this);
+ this.set_popover(popover);
+
+ if (audio_encryption == null) {
+ popover.add(new Label("This call is unencrypted.") { margin=10, visible=true } );
+ return;
+ }
+ if (title != null && !show_keys) {
+ popover.add(new Label(title) { use_markup=true, margin=10, visible=true } );
+ return;
+ }
+
+ Box box = new Box(Orientation.VERTICAL, 10) { margin=10, visible=true };
+ box.add(new Label("<b>%s</b>".printf(title ?? "This call is end-to-end encrypted.")) { use_markup=true, xalign=0, visible=true });
+
+ if (video_encryption == null) {
+ box.add(create_media_encryption_grid(audio_encryption));
+ } else {
+ box.add(new Label("<b>Audio</b>") { use_markup=true, xalign=0, visible=true });
+ box.add(create_media_encryption_grid(audio_encryption));
+ box.add(new Label("<b>Video</b>") { use_markup=true, xalign=0, visible=true });
+ box.add(create_media_encryption_grid(video_encryption));
+ }
+ popover.add(box);
+ }
+
+ private Grid create_media_encryption_grid(Xmpp.Xep.Jingle.ContentEncryption? encryption) {
+ Grid ret = new Grid() { row_spacing=3, column_spacing=5, visible=true };
+ if (encryption.peer_key.length > 0) {
+ ret.attach(new Label("Peer call key") { xalign=0, visible=true }, 1, 2, 1, 1);
+ ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.peer_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1);
+ }
+ if (encryption.our_key.length > 0) {
+ ret.attach(new Label("Your call key") { xalign=0, visible=true }, 1, 3, 1, 1);
+ ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.our_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1);
+ }
+ return ret;
+ }
+
+ private string format_fingerprint(uint8[] fingerprint) {
+ var sb = new StringBuilder();
+ for (int i = 0; i < fingerprint.length; i++) {
+ sb.append("%02x".printf(fingerprint[i]));
+ if (i < fingerprint.length - 1) {
+ sb.append(":");
+ }
+ }
+ return sb.str;
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala
new file mode 100644
index 00000000..3b3d4dc2
--- /dev/null
+++ b/main/src/ui/call_window/call_window.vala
@@ -0,0 +1,260 @@
+using Dino.Entities;
+using Gtk;
+
+namespace Dino.Ui {
+
+ public class CallWindow : Gtk.Window {
+ public string counterpart_display_name { get; set; }
+
+ // TODO should find another place for this
+ public CallWindowController controller;
+
+ public Overlay overlay = new Overlay() { visible=true };
+ public EventBox event_box = new EventBox() { visible=true };
+ public CallBottomBar bottom_bar = new CallBottomBar() { visible=true };
+ public Revealer bottom_bar_revealer = new Revealer() { valign=Align.END, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
+ public HeaderBar header_bar = new HeaderBar() { show_close_button=true, visible=true };
+ public Revealer header_bar_revealer = new Revealer() { valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
+ public Stack stack = new Stack() { visible=true };
+ public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { expand=true, visible=true };
+ private Widget? own_video = null;
+ private Box? own_video_border = new Box(Orientation.HORIZONTAL, 0) { expand=true }; // hack to draw a border around our own video, since we apparently can't draw a border around the Gst widget
+
+ private int own_video_width = 150;
+ private int own_video_height = 100;
+
+ private bool hide_controll_elements = false;
+ private uint hide_controll_handler = 0;
+ private Widget? main_widget = null;
+
+ construct {
+ header_bar.get_style_context().add_class("call-header-bar");
+ header_bar_revealer.add(header_bar);
+
+ this.get_style_context().add_class("dino-call-window");
+
+ bottom_bar_revealer.add(bottom_bar);
+
+ overlay.add_overlay(own_video_box);
+ overlay.add_overlay(own_video_border);
+ overlay.add_overlay(bottom_bar_revealer);
+ overlay.add_overlay(header_bar_revealer);
+
+ event_box.add(overlay);
+ add(event_box);
+
+ Util.force_css(own_video_border, "* { border: 1px solid #616161; background-color: transparent; }");
+ }
+
+ public CallWindow() {
+ event_box.events |= Gdk.EventMask.POINTER_MOTION_MASK;
+ event_box.events |= Gdk.EventMask.ENTER_NOTIFY_MASK;
+ event_box.events |= Gdk.EventMask.LEAVE_NOTIFY_MASK;
+
+ this.bind_property("counterpart-display-name", header_bar, "title", BindingFlags.SYNC_CREATE);
+ this.bind_property("counterpart-display-name", bottom_bar, "counterpart-display-name", BindingFlags.SYNC_CREATE);
+
+ event_box.motion_notify_event.connect(reveal_control_elements);
+ event_box.enter_notify_event.connect(reveal_control_elements);
+ event_box.leave_notify_event.connect(reveal_control_elements);
+ this.configure_event.connect(reveal_control_elements); // upon resizing
+ this.configure_event.connect(update_own_video_position);
+
+ this.set_titlebar(new OutsideHeaderBar(this.header_bar) { visible=true });
+
+ reveal_control_elements();
+ }
+
+ public void set_video_fallback(StreamInteractor stream_interactor, Conversation conversation) {
+ hide_controll_elements = false;
+
+ Box box = new Box(Orientation.HORIZONTAL, 0) { visible=true };
+ box.get_style_context().add_class("video-placeholder-box");
+ AvatarImage avatar = new AvatarImage() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100, visible=true };
+ avatar.set_conversation(stream_interactor, conversation);
+ box.add(avatar);
+
+ set_new_main_widget(box);
+ }
+
+ public void set_video(Widget widget) {
+ hide_controll_elements = true;
+
+ widget.visible = true;
+ set_new_main_widget(widget);
+ }
+
+ public void set_own_video(Widget? widget_) {
+ own_video_box.foreach((widget) => { own_video_box.remove(widget); });
+
+ own_video = widget_;
+ if (own_video == null) {
+ own_video = new Box(Orientation.HORIZONTAL, 0) { expand=true };
+ }
+ own_video.visible = true;
+ own_video.width_request = 150;
+ own_video.height_request = 100;
+ own_video_box.add(own_video);
+
+ own_video_border.visible = true;
+
+ update_own_video_position();
+ }
+
+ public void set_own_video_ratio(int width, int height) {
+ if (width / height > 150 / 100) {
+ this.own_video_width = 150;
+ this.own_video_height = height * 150 / width;
+ } else {
+ this.own_video_width = width * 100 / height;
+ this.own_video_height = 100;
+ }
+
+ own_video.width_request = own_video_width;
+ own_video.height_request = own_video_height;
+
+ update_own_video_position();
+ }
+
+ public void unset_own_video() {
+ own_video_box.foreach((widget) => { own_video_box.remove(widget); });
+
+ own_video_border.visible = false;
+ }
+
+ public void set_test_video() {
+ hide_controll_elements = true;
+
+ var pipeline = new Gst.Pipeline(null);
+ var src = Gst.ElementFactory.make("videotestsrc", null);
+ pipeline.add(src);
+ Gst.Video.Sink sink = (Gst.Video.Sink) Gst.ElementFactory.make("gtksink", null);
+ Gtk.Widget widget;
+ sink.get("widget", out widget);
+ widget.unparent();
+ pipeline.add(sink);
+ src.link(sink);
+ widget.visible = true;
+
+ pipeline.set_state(Gst.State.PLAYING);
+
+ sink.get_static_pad("sink").notify["caps"].connect(() => {
+ int width, height;
+ sink.get_static_pad("sink").caps.get_structure(0).get_int("width", out width);
+ sink.get_static_pad("sink").caps.get_structure(0).get_int("height", out height);
+ widget.width_request = width;
+ widget.height_request = height;
+ });
+
+ set_new_main_widget(widget);
+ }
+
+ private void set_new_main_widget(Widget widget) {
+ if (main_widget != null) overlay.remove(main_widget);
+ overlay.add(widget);
+ main_widget = widget;
+ }
+
+ public void set_status(string state) {
+ switch (state) {
+ case "requested":
+ header_bar.subtitle = _("Calling…");
+ break;
+ case "ringing":
+ header_bar.subtitle = _("Ringing…");
+ break;
+ case "establishing":
+ header_bar.subtitle = _("Connecting…");
+ break;
+ default:
+ header_bar.subtitle = null;
+ break;
+ }
+ }
+
+ public void show_counterpart_ended(string? reason_name, string? reason_text) {
+ hide_controll_elements = false;
+ reveal_control_elements();
+
+ string text = "";
+ if (reason_name == Xmpp.Xep.Jingle.ReasonElement.SUCCESS) {
+ text = _("%s ended the call").printf(counterpart_display_name);
+ } else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) {
+ text = _("%s declined the call").printf(counterpart_display_name);
+ } else {
+ text = "The call has been terminated: " + (reason_name ?? "") + " " + (reason_text ?? "");
+ }
+
+ bottom_bar.show_counterpart_ended(text);
+ }
+
+ public bool reveal_control_elements() {
+ if (!bottom_bar_revealer.child_revealed) {
+ bottom_bar_revealer.set_reveal_child(true);
+ header_bar_revealer.set_reveal_child(true);
+ }
+
+ if (hide_controll_handler != 0) {
+ Source.remove(hide_controll_handler);
+ hide_controll_handler = 0;
+ }
+
+ if (!hide_controll_elements) {
+ return false;
+ }
+
+ hide_controll_handler = Timeout.add_seconds(3, () => {
+ if (!hide_controll_elements) {
+ return false;
+ }
+
+ if (bottom_bar.is_menu_active()) {
+ return true;
+ }
+
+ header_bar_revealer.set_reveal_child(false);
+ bottom_bar_revealer.set_reveal_child(false);
+ hide_controll_handler = 0;
+ return false;
+ });
+ return false;
+ }
+
+ private bool update_own_video_position() {
+ if (own_video == null) return false;
+
+ int width, height;
+ this.get_size(out width,out height);
+
+ own_video.margin_end = own_video.margin_bottom = own_video_border.margin_end = own_video_border.margin_bottom = 20;
+ own_video.margin_start = own_video_border.margin_start = width - own_video_width - 20;
+ own_video.margin_top = own_video_border.margin_top = height - own_video_height - 20;
+
+ return false;
+ }
+ }
+
+ /* Hack to make the CallHeaderBar feel like a HeaderBar (right click menu, double click, ..) although it isn't set as headerbar.
+ * OutsideHeaderBar is set as a headerbar and it doesn't take any space, but claims to take space (which is actually taken by CallHeaderBar).
+ */
+ public class OutsideHeaderBar : Gtk.Box {
+ HeaderBar header_bar;
+
+ public OutsideHeaderBar(HeaderBar header_bar) {
+ this.header_bar = header_bar;
+
+ size_allocate.connect_after(on_header_bar_size_allocate);
+ header_bar.size_allocate.connect(on_header_bar_size_allocate);
+ }
+
+ public void on_header_bar_size_allocate() {
+ Allocation header_bar_alloc;
+ header_bar.get_allocation(out header_bar_alloc);
+
+ Allocation alloc;
+ get_allocation(out alloc);
+ alloc.height = header_bar_alloc.height;
+ set_allocation(alloc);
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala
new file mode 100644
index 00000000..b07b41b1
--- /dev/null
+++ b/main/src/ui/call_window/call_window_controller.vala
@@ -0,0 +1,254 @@
+using Dino.Entities;
+using Gtk;
+
+public class Dino.Ui.CallWindowController : Object {
+
+ private CallWindow call_window;
+ private Call call;
+ private Conversation conversation;
+ private StreamInteractor stream_interactor;
+ private Calls calls;
+ private Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+
+ private Plugins.VideoCallWidget? own_video = null;
+ private Plugins.VideoCallWidget? counterpart_video = null;
+ private int window_height = -1;
+ private int window_width = -1;
+ private bool window_size_changed = false;
+
+ public CallWindowController(CallWindow call_window, Call call, StreamInteractor stream_interactor) {
+ this.call_window = call_window;
+ this.call = call;
+ this.stream_interactor = stream_interactor;
+
+ this.calls = stream_interactor.get_module(Calls.IDENTITY);
+ this.conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(call.counterpart.bare_jid, call.account, Conversation.Type.CHAT);
+ this.own_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
+ this.counterpart_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
+
+ call_window.counterpart_display_name = Util.get_conversation_display_name(stream_interactor, conversation);
+ call_window.set_default_size(704, 528); // 640x480 * 1.1
+ call_window.set_video_fallback(stream_interactor, conversation);
+
+ this.call_window.bottom_bar.video_enabled = calls.should_we_send_video(call);
+
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ call_window.set_status("establishing");
+ } else {
+ call_window.set_status("requested");
+ }
+
+ call_window.bottom_bar.hang_up.connect(() => {
+ calls.end_call(conversation, call);
+ call_window.close();
+ call_window.destroy();
+ this.dispose();
+ });
+ call_window.destroy.connect(() => {
+ calls.end_call(conversation, call);
+ this.dispose();
+ });
+
+ call_window.bottom_bar.notify["audio-enabled"].connect(() => {
+ calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled);
+ });
+ call_window.bottom_bar.notify["video-enabled"].connect(() => {
+ calls.mute_own_video(call, !call_window.bottom_bar.video_enabled);
+ update_own_video();
+ });
+
+ calls.counterpart_sends_video_updated.connect((call, mute) => {
+ if (!this.call.equals(call)) return;
+
+ if (mute) {
+ call_window.set_video_fallback(stream_interactor, conversation);
+ counterpart_video.detach();
+ } else {
+ if (!(counterpart_video is Widget)) return;
+ Widget widget = (Widget) counterpart_video;
+ call_window.set_video(widget);
+ counterpart_video.display_stream(calls.get_video_stream(call));
+ }
+ });
+ calls.info_received.connect((call, session_info) => {
+ if (!this.call.equals(call)) return;
+ if (session_info == Xmpp.Xep.JingleRtp.CallSessionInfo.RINGING) {
+ call_window.set_status("ringing");
+ }
+ });
+ calls.encryption_updated.connect((call, audio_encryption, video_encryption, same) => {
+ if (!this.call.equals(call)) return;
+
+ string? title = null;
+ string? icon_name = null;
+ bool show_keys = true;
+ Plugins.Registry registry = Dino.Application.get_default().plugin_registry;
+ Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null;
+ if (encryption_entry != null) {
+ Plugins.CallEncryptionWidget? encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption);
+ if (encryption_widgets != null) {
+ title = encryption_widgets.get_title();
+ icon_name = encryption_widgets.get_icon_name();
+ show_keys = encryption_widgets.show_keys();
+ }
+ }
+ call_window.bottom_bar.encryption_button.set_info(title, show_keys, audio_encryption, same ? null :video_encryption);
+ call_window.bottom_bar.encryption_button.set_icon(audio_encryption != null, icon_name);
+ });
+
+ own_video.resolution_changed.connect((width, height) => {
+ if (width == 0 || height == 0) return;
+ call_window.set_own_video_ratio((int)width, (int)height);
+ });
+ counterpart_video.resolution_changed.connect((width, height) => {
+ if (window_size_changed) return;
+ if (width == 0 || height == 0) return;
+ if (width > height) {
+ call_window.resize(704, (int) (height * 704 / width));
+ } else {
+ call_window.resize((int) (width * 704 / height), 704);
+ }
+ capture_window_size();
+ });
+ call_window.configure_event.connect((event) => {
+ if (window_width == -1 || window_height == -1) return false;
+ int current_height = this.call_window.get_allocated_height();
+ int current_width = this.call_window.get_allocated_width();
+ if (window_width != current_width || window_height != current_height) {
+ debug("Call window size changed by user. Disabling auto window-to-video size adaptation. %i->%i x %i->%i", window_width, current_width, window_height, current_height);
+ window_size_changed = true;
+ }
+ return false;
+ });
+ call_window.realize.connect(() => {
+ capture_window_size();
+ });
+
+ call.notify["state"].connect(on_call_state_changed);
+ calls.call_terminated.connect(on_call_terminated);
+
+ update_own_video();
+ }
+
+ private void capture_window_size() {
+ Allocation allocation;
+ this.call_window.get_allocation(out allocation);
+ this.window_height = this.call_window.get_allocated_height();
+ this.window_width = this.call_window.get_allocated_width();
+ }
+
+ private void on_call_state_changed() {
+ if (call.state == Call.State.IN_PROGRESS) {
+ call_window.set_status("");
+ call_plugin.devices_changed.connect((media, incoming) => {
+ if (media == "audio") update_audio_device_choices();
+ if (media == "video") update_video_device_choices();
+ });
+
+ update_audio_device_choices();
+ update_video_device_choices();
+ }
+ }
+
+ private void on_call_terminated(Call call, string? reason_name, string? reason_text) {
+ call_window.show_counterpart_ended(reason_name, reason_text);
+ Timeout.add_seconds(3, () => {
+ call.notify["state"].disconnect(on_call_state_changed);
+ calls.call_terminated.disconnect(on_call_terminated);
+
+
+ call_window.close();
+ call_window.destroy();
+
+ return false;
+ });
+ }
+
+ private void update_audio_device_choices() {
+ if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) {
+ call_window.bottom_bar.show_audio_device_error();
+ } /*else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) {
+ call_window.bottom_bar.show_audio_device_choices(false);
+ return;
+ }
+
+ AudioSettingsPopover? audio_settings_popover = call_window.bottom_bar.show_audio_device_choices(true);
+ update_current_audio_device(audio_settings_popover);
+
+ audio_settings_popover.microphone_selected.connect((device) => {
+ call_plugin.set_device(calls.get_audio_stream(call), device);
+ update_current_audio_device(audio_settings_popover);
+ });
+ audio_settings_popover.speaker_selected.connect((device) => {
+ call_plugin.set_device(calls.get_audio_stream(call), device);
+ update_current_audio_device(audio_settings_popover);
+ });
+ calls.stream_created.connect((call, media) => {
+ if (media == "audio") {
+ update_current_audio_device(audio_settings_popover);
+ }
+ });*/
+ }
+
+ private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
+ Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call);
+ if (stream != null) {
+ audio_settings_popover.current_microphone_device = call_plugin.get_device(stream, false);
+ audio_settings_popover.current_speaker_device = call_plugin.get_device(stream, true);
+ }
+ }
+
+ private void update_video_device_choices() {
+ int device_count = call_plugin.get_devices("video", false).size;
+
+ if (device_count == 0) {
+ call_window.bottom_bar.show_video_device_error();
+ } /*else if (device_count == 1 || calls.get_video_stream(call) == null) {
+ call_window.bottom_bar.show_video_device_choices(false);
+ return;
+ }
+
+ VideoSettingsPopover? video_settings_popover = call_window.bottom_bar.show_video_device_choices(true);
+ update_current_video_device(video_settings_popover);
+
+ video_settings_popover.camera_selected.connect((device) => {
+ call_plugin.set_device(calls.get_video_stream(call), device);
+ update_current_video_device(video_settings_popover);
+ own_video.display_device(device);
+ });
+ calls.stream_created.connect((call, media) => {
+ if (media == "video") {
+ update_current_video_device(video_settings_popover);
+ }
+ });*/
+ }
+
+ private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
+ Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call);
+ if (stream != null) {
+ video_settings_popover.current_device = call_plugin.get_device(stream, false);
+ }
+ }
+
+ private void update_own_video() {
+ if (this.call_window.bottom_bar.video_enabled) {
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
+ if (!(own_video is Widget) || devices.is_empty) {
+ call_window.set_own_video(null);
+ } else {
+ Widget widget = (Widget) own_video;
+ call_window.set_own_video(widget);
+ own_video.display_device(devices.first());
+ }
+ } else {
+ own_video.detach();
+ call_window.unset_own_video();
+ }
+ }
+
+ public override void dispose() {
+ base.dispose();
+ call.notify["state"].disconnect(on_call_state_changed);
+ calls.call_terminated.disconnect(on_call_terminated);
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/call_window/video_settings_popover.vala b/main/src/ui/call_window/video_settings_popover.vala
new file mode 100644
index 00000000..396c697c
--- /dev/null
+++ b/main/src/ui/call_window/video_settings_popover.vala
@@ -0,0 +1,73 @@
+using Gee;
+using Gtk;
+using Dino.Entities;
+
+public class Dino.Ui.VideoSettingsPopover : Gtk.Popover {
+
+ public signal void camera_selected(Plugins.MediaDevice device);
+
+ public Plugins.MediaDevice? current_device { get; set; }
+
+ private HashMap<ListBoxRow, Plugins.MediaDevice> row_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
+
+ public VideoSettingsPopover() {
+ Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
+ box.add(create_camera_box());
+
+ this.add(box);
+ }
+
+ private Widget create_camera_box() {
+ Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
+ Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
+
+ Box camera_box = new Box(Orientation.VERTICAL, 10) { visible=true };
+ camera_box.add(new Label("<b>" + _("Cameras") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
+
+ if (devices.size == 0) {
+ camera_box.add(new Label("No cameras found.") { visible=true });
+ } else {
+ ListBox list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
+ list_box.set_header_func(listbox_header_func);
+ Frame frame = new Frame(null) { visible=true };
+ frame.add(list_box);
+ foreach (Plugins.MediaDevice device in devices) {
+ Label label = new Label(device.display_name) { xalign=0, visible=true };
+ Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
+ if (current_device == null || current_device.id != device.id) {
+ image.opacity = 0;
+ }
+ this.notify["current-device"].connect(() => {
+ if (current_device == null || current_device.id != device.id) {
+ image.opacity = 0;
+ } else {
+ image.opacity = 1;
+ }
+ });
+ Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
+ device_box.add(image);
+ device_box.add(label);
+ ListBoxRow list_box_row = new ListBoxRow() { visible=true };
+ list_box_row.add(device_box);
+ list_box.add(list_box_row);
+
+ row_device[list_box_row] = device;
+ }
+ list_box.row_activated.connect((row) => {
+ if (!row_device.has_key(row)) return;
+ camera_selected(row_device[row]);
+ list_box.unselect_row(row);
+ });
+ camera_box.add(frame);
+ }
+
+ return camera_box;
+ }
+
+ private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
+ if (row.get_header() == null && before_row != null) {
+ row.set_header(new Separator(Orientation.HORIZONTAL));
+ }
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala
new file mode 100644
index 00000000..74525d11
--- /dev/null
+++ b/main/src/ui/conversation_content_view/call_widget.vala
@@ -0,0 +1,215 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+ public class CallMetaItem : ConversationSummary.ContentMetaItem {
+
+ private StreamInteractor stream_interactor;
+
+ public CallMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
+ base(content_item);
+ this.stream_interactor = stream_interactor;
+ }
+
+ public override Object? get_widget(Plugins.WidgetType type) {
+ CallItem call_item = content_item as CallItem;
+ return new CallWidget(stream_interactor, call_item.call, call_item.conversation) { visible=true };
+ }
+
+ public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
+ }
+
+ [GtkTemplate (ui = "/im/dino/Dino/call_widget.ui")]
+ public class CallWidget : SizeRequestBox {
+
+ [GtkChild] public Image image;
+ [GtkChild] public Label title_label;
+ [GtkChild] public Label subtitle_label;
+ [GtkChild] public Revealer incoming_call_revealer;
+ [GtkChild] public Button accept_call_button;
+ [GtkChild] public Button reject_call_button;
+
+ private StreamInteractor stream_interactor;
+ private Call call;
+ private Conversation conversation;
+ public Call.State call_state { get; set; } // needs to be public for binding
+ private uint time_update_handler_id = 0;
+
+ construct {
+ margin_top = 4;
+ size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+
+ public CallWidget(StreamInteractor stream_interactor, Call call, Conversation conversation) {
+ this.stream_interactor = stream_interactor;
+ this.call = call;
+ this.conversation = conversation;
+
+ size_allocate.connect((allocation) => {
+ if (allocation.height > parent.get_allocated_height()) {
+ Idle.add(() => { parent.queue_resize(); return false; });
+ }
+ });
+
+ call.bind_property("state", this, "call-state");
+ this.notify["call-state"].connect(update_widget);
+
+ accept_call_button.clicked.connect(() => {
+ stream_interactor.get_module(Calls.IDENTITY).accept_call(call);
+
+ var call_window = new CallWindow();
+ call_window.controller = new CallWindowController(call_window, call, stream_interactor);
+ call_window.present();
+ });
+
+ reject_call_button.clicked.connect(() => {
+ stream_interactor.get_module(Calls.IDENTITY).reject_call(call);
+ });
+
+ update_widget();
+ }
+
+ private void update_widget() {
+ incoming_call_revealer.reveal_child = false;
+ incoming_call_revealer.get_style_context().remove_class("incoming");
+
+ switch (call.state) {
+ case Call.State.RINGING:
+ image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ bool video = stream_interactor.get_module(Calls.IDENTITY).should_we_send_video(call);
+ title_label.label = video ? _("Video call incoming") : _("Call incoming");
+ subtitle_label.label = "Ring ring…!";
+ incoming_call_revealer.reveal_child = true;
+ incoming_call_revealer.get_style_context().add_class("incoming");
+ } else {
+ title_label.label = _("Establishing call");
+ subtitle_label.label = "Ring ring…?";
+ }
+ break;
+ case Call.State.ESTABLISHING:
+ image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ bool video = stream_interactor.get_module(Calls.IDENTITY).should_we_send_video(call);
+ title_label.label = video ? _("Video call establishing") : _("Call establishing");
+ subtitle_label.label = "Connecting…";
+ }
+ break;
+ case Call.State.IN_PROGRESS:
+ image.set_from_icon_name("dino-phone-in-talk-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = _("Call in progress…");
+ string duration = get_duration_string((new DateTime.now_utc()).difference(call.local_time));
+ subtitle_label.label = _("Started %s ago").printf(duration);
+
+ time_update_handler_id = Timeout.add_seconds(get_next_time_change() + 1, () => {
+ Source.remove(time_update_handler_id);
+ time_update_handler_id = 0;
+ update_widget();
+ return true;
+ });
+
+ break;
+ case Call.State.OTHER_DEVICE_ACCEPTED:
+ image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = call.direction == Call.DIRECTION_INCOMING ? _("Incoming call") : _("Outgoing call");
+ subtitle_label.label = _("You handled this call on another device");
+
+ break;
+ case Call.State.ENDED:
+ image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = _("Call ended");
+ string formated_end = Util.format_time(call.end_time, _("%H∶%M"), _("%l∶%M %p"));
+ string duration = get_duration_string(call.end_time.difference(call.local_time));
+ subtitle_label.label = _("Ended at %s").printf(formated_end) +
+ " · " +
+ _("Lasted for %s").printf(duration);
+ break;
+ case Call.State.MISSED:
+ image.set_from_icon_name("dino-phone-missed-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = _("Call missed");
+ string who = null;
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ who = "You";
+ } else {
+ who = Util.get_participant_display_name(stream_interactor, conversation, call.to);
+ }
+ subtitle_label.label = "%s missed this call".printf(who);
+ break;
+ case Call.State.DECLINED:
+ image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = _("Call declined");
+ string who = null;
+ if (call.direction == Call.DIRECTION_INCOMING) {
+ who = "You";
+ } else {
+ who = Util.get_participant_display_name(stream_interactor, conversation, call.to);
+ }
+ subtitle_label.label = "%s declined this call".printf(who);
+ break;
+ case Call.State.FAILED:
+ image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
+ title_label.label = _("Call failed");
+ subtitle_label.label = "Call failed to establish";
+ break;
+ }
+ }
+
+ private string get_duration_string(TimeSpan duration) {
+ DateTime a = new DateTime.now_utc();
+ DateTime b = new DateTime.now_utc();
+ a.difference(b);
+
+ TimeSpan remainder_duration = duration;
+
+ int hours = (int) Math.floor(remainder_duration / TimeSpan.HOUR);
+ remainder_duration -= hours * TimeSpan.HOUR;
+
+ int minutes = (int) Math.floor(remainder_duration / TimeSpan.MINUTE);
+ remainder_duration -= minutes * TimeSpan.MINUTE;
+
+ string ret = "";
+
+ if (hours > 0) {
+ ret += n("%i hour", "%i hours", hours).printf(hours);
+ }
+
+ if (minutes > 0) {
+ if (ret.length > 0) {
+ ret += " ";
+ }
+ ret += n("%i minute", "%i minutes", minutes).printf(minutes);
+ }
+
+ if (ret.length > 0) {
+ return ret;
+ }
+
+ return _("seconds");
+ }
+
+ private int get_next_time_change() {
+ DateTime now = new DateTime.now_local();
+ DateTime item_time = call.local_time;
+
+ if (now.get_second() < item_time.get_second()) {
+ return item_time.get_second() - now.get_second();
+ } else {
+ return 60 - (now.get_second() - item_time.get_second());
+ }
+ }
+
+ public override void dispose() {
+ base.dispose();
+
+ if (time_update_handler_id != 0) {
+ Source.remove(time_update_handler_id);
+ time_update_handler_id = 0;
+ }
+ }
+ }
+}
diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala
index 97f15bf9..ef859bde 100644
--- a/main/src/ui/conversation_content_view/content_populator.vala
+++ b/main/src/ui/conversation_content_view/content_populator.vala
@@ -68,7 +68,10 @@ public class ContentProvider : ContentItemCollection, Object {
return new MessageMetaItem(content_item, stream_interactor);
} else if (content_item.type_ == FileItem.TYPE) {
return new FileMetaItem(content_item, stream_interactor);
+ } else if (content_item.type_ == CallItem.TYPE) {
+ return new CallMetaItem(content_item, stream_interactor);
}
+ critical("Got unknown content item type %s", content_item.type_);
return null;
}
}
@@ -85,6 +88,7 @@ public abstract class ContentMetaItem : Plugins.MetaConversationItem {
this.mark = content_item.mark;
content_item.bind_property("mark", this, "mark");
+ content_item.bind_property("encryption", this, "encryption");
this.can_merge = true;
this.requires_avatar = true;
diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala
index c0099bf4..bcb6864e 100644
--- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala
+++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala
@@ -104,7 +104,7 @@ public class ItemMetaDataHeader : Box {
[GtkChild] public Label dot_label;
[GtkChild] public Label time_label;
public Image received_image = new Image() { opacity=0.4 };
- public Image? unencrypted_image = null;
+ public Widget? encryption_image = null;
public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12);
@@ -124,50 +124,66 @@ public class ItemMetaDataHeader : Box {
update_name_label();
name_label.style_updated.connect(update_name_label);
+ conversation.notify["encryption"].connect(update_unencrypted_icon);
+ item.notify["encryption"].connect(update_encryption_icon);
+ update_encryption_icon();
+
+ this.add(received_image);
+
+ if (item.time != null) {
+ update_time();
+ }
+
+ item.bind_property("mark", this, "item-mark");
+ this.notify["item-mark"].connect_after(update_received_mark);
+ update_received_mark();
+ }
+
+ private void update_encryption_icon() {
Application app = GLib.Application.get_default() as Application;
ContentMetaItem ci = item as ContentMetaItem;
- if (ci != null) {
+ if (item.encryption != Encryption.NONE && ci != null) {
+ Widget? widget = null;
foreach(var e in app.plugin_registry.encryption_list_entries) {
if (e.encryption == item.encryption) {
- Object? w = e.get_encryption_icon(conversation, ci.content_item);
- if (w != null) {
- this.add(w as Widget);
- } else {
- Image image = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
- this.add(image);
- }
+ widget = e.get_encryption_icon(conversation, ci.content_item) as Widget;
break;
}
}
+ if (widget == null) {
+ widget = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
+ }
+ update_encryption_image(widget);
}
if (item.encryption == Encryption.NONE) {
- conversation.notify["encryption"].connect(update_unencrypted_icon);
update_unencrypted_icon();
}
+ }
- this.add(received_image);
-
- if (item.time != null) {
- update_time();
+ private void update_unencrypted_icon() {
+ if (item.encryption != Encryption.NONE) return;
+
+ if (conversation.encryption != Encryption.NONE && encryption_image == null) {
+ Image image = new Image() { opacity=0.4, visible = true };
+ image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER);
+ image.tooltip_text = _("Unencrypted");
+ update_encryption_image(image);
+ Util.force_error_color(image);
+ } else if (conversation.encryption == Encryption.NONE && encryption_image != null) {
+ update_encryption_image(null);
}
-
- item.bind_property("mark", this, "item-mark");
- this.notify["item-mark"].connect_after(update_received_mark);
- update_received_mark();
}
- private void update_unencrypted_icon() {
- if (conversation.encryption != Encryption.NONE && unencrypted_image == null) {
- unencrypted_image = new Image() { opacity=0.4, visible = true };
- unencrypted_image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER);
- unencrypted_image.tooltip_text = _("Unencrypted");
- this.add(unencrypted_image);
- this.reorder_child(unencrypted_image, 3);
- Util.force_error_color(unencrypted_image);
- } else if (conversation.encryption == Encryption.NONE && unencrypted_image != null) {
- this.remove(unencrypted_image);
- unencrypted_image = null;
+ private void update_encryption_image(Widget? widget) {
+ if (encryption_image != null) {
+ this.remove(encryption_image);
+ encryption_image = null;
+ }
+ if (widget != null) {
+ this.add(widget);
+ this.reorder_child(widget, 3);
+ encryption_image = widget;
}
}
diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala
index 9b748876..7d77ba11 100644
--- a/main/src/ui/conversation_content_view/file_widget.vala
+++ b/main/src/ui/conversation_content_view/file_widget.vala
@@ -32,9 +32,6 @@ public class FileWidget : SizeRequestBox {
DEFAULT
}
- private const int MAX_HEIGHT = 300;
- private const int MAX_WIDTH = 600;
-
private StreamInteractor stream_interactor;
private FileTransfer file_transfer;
public FileTransfer.State file_transfer_state { get; set; }
diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala
index cd513d13..6f181a64 100644
--- a/main/src/ui/conversation_selector/conversation_selector_row.vala
+++ b/main/src/ui/conversation_selector/conversation_selector_row.vala
@@ -198,6 +198,14 @@ public class ConversationSelectorRow : ListBoxRow {
message_label.label = (file_is_image ? _("Image received") : _("File received") );
}
break;
+ case CallItem.TYPE:
+ CallItem call_item = (CallItem) last_content_item;
+ Call call = call_item.call;
+
+ nick_label.label = call.direction == Call.DIRECTION_OUTGOING ? _("Me") + ": " : "";
+ message_label.attributes.insert(attr_style_new(Pango.Style.ITALIC));
+ message_label.label = call.direction == Call.DIRECTION_OUTGOING ? _("Outgoing call") : _("Incoming call");
+ break;
}
nick_label.visible = true;
message_label.visible = true;
diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala
new file mode 100644
index 00000000..9353f631
--- /dev/null
+++ b/main/src/ui/conversation_titlebar/call_entry.vala
@@ -0,0 +1,132 @@
+using Xmpp;
+using Gtk;
+using Gee;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+ public class CallTitlebarEntry : Plugins.ConversationTitlebarEntry, Object {
+ public string id { get { return "call"; } }
+
+ public CallButton call_button;
+
+ private StreamInteractor stream_interactor;
+
+ public CallTitlebarEntry(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ call_button = new CallButton(stream_interactor) { tooltip_text=_("Start call") };
+ call_button.set_image(new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true });
+ }
+
+ public double order { get { return 4; } }
+ public Plugins.ConversationTitlebarWidget? get_widget(Plugins.WidgetType type) {
+ if (type == Plugins.WidgetType.GTK) {
+ return call_button;
+ }
+ return null;
+ }
+ }
+
+ public class CallButton : Plugins.ConversationTitlebarWidget, Gtk.MenuButton {
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+
+ private ModelButton audio_button = new ModelButton() { text="Audio call", visible=true };
+ private ModelButton video_button = new ModelButton() { text="Video call", visible=true };
+
+ public CallButton(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ use_popover = true;
+ image = new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true };
+
+ Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu();
+ Box box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true };
+ audio_button.clicked.connect(() => {
+ stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, false, (_, res) => {
+ Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res);
+ open_call_window(call);
+ });
+ });
+ box.add(audio_button);
+
+ video_button.clicked.connect(() => {
+ stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, true, (_, res) => {
+ Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res);
+ open_call_window(call);
+ });
+ });
+ box.add(video_button);
+ popover_menu.add(box);
+
+ popover = popover_menu;
+
+ clicked.connect(() => {
+ popover_menu.visible = true;
+ });
+
+ stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect((call, conversation) => {
+ update_button_state();
+ });
+
+ stream_interactor.get_module(Calls.IDENTITY).call_terminated.connect((call) => {
+ update_button_state();
+ });
+ stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((jid, account) => {
+ if (this.conversation.counterpart.equals_bare(jid) && this.conversation.account.equals(account)) {
+ update_visibility.begin();
+ }
+ });
+ stream_interactor.connection_manager.connection_state_changed.connect((account, state) => {
+ update_visibility.begin();
+ });
+ }
+
+ private void open_call_window(Call call) {
+ var call_window = new CallWindow();
+ var call_controller = new CallWindowController(call_window, call, stream_interactor);
+ call_window.controller = call_controller;
+ call_window.present();
+
+ update_button_state();
+ }
+
+ public new void set_conversation(Conversation conversation) {
+ this.conversation = conversation;
+
+ update_visibility.begin();
+ update_button_state();
+ }
+
+ private void update_button_state() {
+ Jid? call_counterpart = stream_interactor.get_module(Calls.IDENTITY).is_call_in_progress();
+ this.sensitive = call_counterpart == null;
+
+ if (call_counterpart != null && call_counterpart.equals_bare(conversation.counterpart)) {
+ this.set_image(new Gtk.Image.from_icon_name("dino-phone-in-talk-symbolic", Gtk.IconSize.MENU) { visible=true });
+ } else {
+ this.set_image(new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true });
+ }
+ }
+
+ private async void update_visibility() {
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ Conversation conv_bak = conversation;
+ bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
+ bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
+ if (conv_bak != conversation) return;
+
+ visible = audio_works;
+ video_button.visible = video_works;
+ } else {
+ visible = false;
+ }
+ }
+
+ public new void unset_conversation() { }
+ }
+
+}
diff --git a/main/src/ui/conversation_view_controller.vala b/main/src/ui/conversation_view_controller.vala
index dcd3e1c7..a9a94738 100644
--- a/main/src/ui/conversation_view_controller.vala
+++ b/main/src/ui/conversation_view_controller.vala
@@ -87,6 +87,7 @@ public class ConversationViewController : Object {
app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor));
app.plugin_registry.register_contact_titlebar_entry(search_menu_entry);
app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor));
+ app.plugin_registry.register_contact_titlebar_entry(new CallTitlebarEntry(stream_interactor));
foreach(var entry in app.plugin_registry.conversation_titlebar_entries) {
titlebar.insert_entry(entry);
}
diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala
index ecb5dc66..78ed2d1e 100644
--- a/main/src/ui/notifier_freedesktop.vala
+++ b/main/src/ui/notifier_freedesktop.vala
@@ -14,6 +14,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
private HashMap<Conversation, uint32> content_notifications = new HashMap<Conversation, uint32>(Conversation.hash_func, Conversation.equals_func);
private HashMap<Conversation, Gee.List<uint32>> conversation_notifications = new HashMap<Conversation, Gee.List<uint32>>(Conversation.hash_func, Conversation.equals_func);
private HashMap<uint32, HashMap<string, ListenerFuncWrapper>> action_listeners = new HashMap<uint32, HashMap<string, ListenerFuncWrapper>>();
+ private HashMap<Call, uint32> call_notifications = new HashMap<Call, uint32>(Call.hash_func, Call.equals_func);
private FreeDesktopNotifier(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
@@ -110,6 +111,43 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
}
}
+ public async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name) {
+ string summary = Markup.escape_text(conversation_display_name);
+ string body = video ? _("Incoming video call") : _("Incoming call");
+
+ HashTable<string, Variant> hash_table = new HashTable<string, Variant>(null, null);
+ hash_table["image-path"] = "call-start-symbolic";
+ hash_table["sound-name"] = new Variant.string("phone-incoming-call");
+ hash_table["urgency"] = new Variant.byte(2);
+ string[] actions = new string[] {"default", "Open conversation", "reject", _("Reject"), "accept", _("Accept")};
+ try {
+ uint32 notification_id = dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, 0);
+ call_notifications[call] = notification_id;
+
+ add_action_listener(notification_id, "default", () => {
+ GLib.Application.get_default().activate_action("open-conversation", new Variant.int32(conversation.id));
+ });
+ add_action_listener(notification_id, "reject", () => {
+ GLib.Application.get_default().activate_action("deny-call", new Variant.int32(call.id));
+ });
+ add_action_listener(notification_id, "accept", () => {
+ GLib.Application.get_default().activate_action("accept-call", new Variant.int32(call.id));
+ });
+ } catch (Error e) {
+ warning("Failed showing subscription request notification: %s", e.message);
+ }
+ }
+
+ public async void retract_call_notification(Call call, Conversation conversation) {
+ if (!call_notifications.has_key(call)) return;
+ uint32 notification_id = call_notifications[call];
+ try {
+ dbus_notifications.close_notification(notification_id);
+ action_listeners.unset(notification_id);
+ call_notifications.unset(call);
+ } catch (Error e) { }
+ }
+
public async void notify_subscription_request(Conversation conversation) {
string summary = _("Subscription request");
string body = Markup.escape_text(conversation.counterpart.to_string());
diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala
index 31d1ffa3..5fd3be4b 100644
--- a/main/src/ui/notifier_gnotifications.vala
+++ b/main/src/ui/notifier_gnotifications.vala
@@ -65,6 +65,25 @@ namespace Dino.Ui {
}
}
+ public async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name) {
+ Notification notification = new Notification(conversation_display_name);
+ string body = _("Incoming call");
+ notification.set_body(body);
+ notification.set_urgent(true);
+
+ notification.set_icon(new ThemedIcon.from_names(new string[] {"call-start-symbolic"}));
+
+ notification.set_default_action_and_target_value("app.open-conversation", new Variant.int32(conversation.id));
+ notification.add_button_with_target_value(_("Deny"), "app.deny-call", new Variant.int32(call.id));
+ notification.add_button_with_target_value(_("Accept"), "app.accept-call", new Variant.int32(call.id));
+
+ GLib.Application.get_default().send_notification(call.id.to_string(), notification);
+ }
+
+ private async void retract_call_notification(Call call, Conversation conversation) {
+ GLib.Application.get_default().withdraw_notification(call.id.to_string());
+ }
+
public async void notify_subscription_request(Conversation conversation) {
Notification notification = new Notification(_("Subscription request"));
notification.set_body(conversation.counterpart.to_string());
diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala
index b6c9cb5a..17dfd334 100644
--- a/main/src/ui/util/helper.vala
+++ b/main/src/ui/util/helper.vala
@@ -122,15 +122,15 @@ public static string get_participant_display_name(StreamInteractor stream_intera
return Dino.get_participant_display_name(stream_interactor, conversation, participant, me_is_me ? _("Me") : null);
}
-private static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) {
+public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) {
return Dino.get_real_display_name(stream_interactor, account, jid, me_is_me ? _("Me") : null);
}
-private static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
+public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
return Dino.get_groupchat_display_name(stream_interactor, account, jid);
}
-private static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) {
+public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) {
return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null);
}
@@ -194,6 +194,15 @@ public static bool is_24h_format() {
return is24h == 1;
}
+public static string format_time(DateTime datetime, string format_24h, string format_12h) {
+ string format = Util.is_24h_format() ? format_24h : format_12h;
+ if (!get_charset(null)) {
+ // No UTF-8 support, use simple colon for time instead
+ format = format.replace("∶", ":");
+ }
+ return datetime.format(format);
+}
+
public static Regex get_url_regex() {
if (URL_REGEX == null) {
URL_REGEX = /\b(((http|ftp)s?:\/\/|(ircs?|xmpp|mailto|sms|smsto|mms|tel|geo|openpgp4fpr|im|news|nntp|sip|ssh|bitcoin|sftp|magnet|vnc|urn):)\S+)/;