diff options
author | fiaxh <fiaxh@users.noreply.github.com> | 2018-08-31 16:25:51 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-31 16:25:51 +0200 |
commit | ecb18afdb51b3230ea451a27a5b345cf5100f02e (patch) | |
tree | 44bae666c4cc31afce3a5d6891f6b4f65dfcebb1 /main | |
parent | 9e93a77a624aed24402cf1ece69c05aaa0aab600 (diff) | |
parent | f5547076d2397cec5c9d7374bd161f93327488c4 (diff) | |
download | dino-ecb18afdb51b3230ea451a27a5b345cf5100f02e.tar.gz dino-ecb18afdb51b3230ea451a27a5b345cf5100f02e.zip |
Merge pull request #415 from bobufa/message-search
Message search
Diffstat (limited to 'main')
32 files changed, 1225 insertions, 666 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 00ca692a..65d84bdd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -29,6 +29,7 @@ set(RESOURCE_LIST chat_input.ui contact_details_dialog.ui conversation_list_titlebar.ui + global_search.ui conversation_selector/view.ui conversation_selector/chat_row_tooltip.ui conversation_selector/conversation_row.ui @@ -43,7 +44,9 @@ set(RESOURCE_LIST menu_encryption.ui occupant_list.ui occupant_list_item.ui + search_autocomplete.ui settings_dialog.ui + unified_main_content.ui unified_window_placeholder.ui theme.css @@ -93,6 +96,7 @@ SOURCES src/ui/contact_details/dialog.vala src/ui/contact_details/muc_config_form_provider.vala src/ui/conversation_list_titlebar.vala + src/ui/global_search.vala src/ui/conversation_selector/chat_row.vala src/ui/conversation_selector/conversation_row.vala src/ui/conversation_selector/groupchat_pm_row.vala @@ -100,19 +104,16 @@ SOURCES src/ui/conversation_selector/list.vala src/ui/conversation_selector/view.vala src/ui/conversation_summary/chat_state_populator.vala + src/ui/conversation_summary/content_item_widget_factory.vala + src/ui/conversation_summary/content_populator.vala src/ui/conversation_summary/conversation_item_skeleton.vala src/ui/conversation_summary/conversation_view.vala src/ui/conversation_summary/date_separator_populator.vala - src/ui/conversation_summary/default_file_display.vala - src/ui/conversation_summary/default_message_display.vala - src/ui/conversation_summary/file_populator.vala - src/ui/conversation_summary/image_display.vala - src/ui/conversation_summary/message_populator.vala src/ui/conversation_summary/message_textview.vala - src/ui/conversation_summary/slashme_message_display.vala src/ui/conversation_summary/subscription_notification.vala src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/occupants_entry.vala + src/ui/conversation_titlebar/search_entry.vala src/ui/conversation_titlebar/view.vala src/ui/manage_accounts/account_row.vala src/ui/manage_accounts/add_account_dialog.vala diff --git a/main/data/conversation_list_titlebar.ui b/main/data/conversation_list_titlebar.ui index f8fabedc..6c5d2d0a 100644 --- a/main/data/conversation_list_titlebar.ui +++ b/main/data/conversation_list_titlebar.ui @@ -22,20 +22,5 @@ <property name="pack_type">start</property> </packing> </child> - <child> - <object class="GtkToggleButton" id="search_button"> - <property name="visible">True</property> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="icon-name">system-search-symbolic</property> - <property name="icon-size">1</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">end</property> - </packing> - </child> </template> </interface> diff --git a/main/data/conversation_selector/view.ui b/main/data/conversation_selector/view.ui index 365957a8..c5560ad1 100644 --- a/main/data/conversation_selector/view.ui +++ b/main/data/conversation_selector/view.ui @@ -5,21 +5,6 @@ <property name="expand">True</property> <property name="orientation">vertical</property> <child> - <object class="GtkRevealer" id="search_revealer"> - <property name="hexpand">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="margin">10px</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> diff --git a/main/data/global_search.ui b/main/data/global_search.ui new file mode 100644 index 00000000..44abf6de --- /dev/null +++ b/main/data/global_search.ui @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="DinoUiGlobalSearch" parent="GtkOverlay"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="visible">True</property> + <property name="margin">12</property> + </object> + </child> + <child> + <object class="GtkStack" id="results_empty_stack"> + <property name="visible">True</property> + <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-search-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" translatable="yes">No active search</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="visible">True</property> + <attributes> + <attribute name="weight" value="PANGO_WEIGHT_BOLD"/> + <attribute name="scale" value="1.3"/> + </attributes> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Type to start a search</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> + </object> + <packing> + <property name="name">empty</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">face-uncertain-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" translatable="yes">No matching messages</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="visible">True</property> + <attributes> + <attribute name="weight" value="PANGO_WEIGHT_BOLD"/> + <attribute name="scale" value="1.3"/> + </attributes> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Check the spelling or try to remove filters</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> + </object> + <packing> + <property name="name">no-result</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="entry_number_label"> + <property name="xalign">0</property> + <property name="use-markup">True</property> + <property name="margin-left">17</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkScrolledWindow" id="results_scrolled"> + <property name="hscrollbar-policy">never</property> + <property name="expand">True</property> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="results_box"> + <property name="orientation">vertical</property> + <property name="spacing">25</property> + <property name="margin">10</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">results</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="overlay"> + <object class="GtkFrame" id="auto_complete_overlay"> + <property name="visible">True</property> + <property name="margin-top">42</property> + <property name="margin-left">12</property> + <property name="margin-right">12</property> + <property name="valign">start</property> + <style> + <class name="auto-complete"/> + </style> + <child> + <object class="GtkListBox" id="auto_complete_list"> + <property name="visible">True</property> + <property name="selection-mode">browse</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/main/data/menu_add.ui b/main/data/menu_add.ui index d8fd691b..fdf01352 100644 --- a/main/data/menu_add.ui +++ b/main/data/menu_add.ui @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> <interface> <menu id="menu_add"> <section> diff --git a/main/data/menu_app.ui b/main/data/menu_app.ui index beb81f3f..eb862ddb 100644 --- a/main/data/menu_app.ui +++ b/main/data/menu_app.ui @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> <interface> <menu id="menu_app"> <section> diff --git a/main/data/menu_conversation.ui b/main/data/menu_conversation.ui index 42b580be..a65522c3 100644 --- a/main/data/menu_conversation.ui +++ b/main/data/menu_conversation.ui @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> <interface> <menu id="menu_conversation"> <section> diff --git a/main/data/search_autocomplete.ui b/main/data/search_autocomplete.ui new file mode 100644 index 00000000..94ec5d7f --- /dev/null +++ b/main/data/search_autocomplete.ui @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="GtkBox" id="root"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <child> + <object class="DinoUiAvatarImage" id="image"> + <property name="margin">4</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="height">24</property> + <property name="width">24</property> + <property name="visible">True</property> + <property name="allow_gray">False</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + </object> + </child> + </object> +</interface>
\ No newline at end of file diff --git a/main/data/theme.css b/main/data/theme.css index e7d58ffb..226689b3 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -17,12 +17,47 @@ window.dino-main .dino-conversation undershoot { background: none; } -window.dino-main .dino-chatinput frame box { +@keyframes highlight { + from { background: alpha(@warning_color, 0.5) } + to { background: transparent } +} + +window.dino-main .dino-conversation .highlight-once { + animation-duration: 3s; + animation-timing-function: ease-out; + animation-iteration-count: 1; + animation-name: highlight; +} + +window.dino-main .dino-conversation textview, window.dino-main .dino-conversation textview text { + background: transparent; +} + +window.dino-main .dino-sidebar > frame { + background: @insensitive_bg_color; + border-left: 1px solid @borders; + border-bottom: 1px solid @borders; +} + +window.dino-main .dino-sidebar > frame.collapsed { + border-bottom: 1px solid @borders; +} + +window.dino-main .dino-sidebar frame.auto-complete { background: @theme_base_color; } -window.dino-main .dino-chatinput frame box:backdrop { - background: @theme_unfocused_base_color; +window.dino-main .dino-sidebar frame.auto-complete list > row { + transition: none; +} + +window.dino-main .dino-sidebar textview, +window.dino-main .dino-sidebar textview text { + background-color: transparent; +} + +window.dino-main .dino-chatinput frame box { + background: transparent; } window.dino-main button.dino-chatinput-button { diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui new file mode 100644 index 00000000..b2f3a891 --- /dev/null +++ b/main/data/unified_main_content.ui @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="GtkPaned" id="paned"> + <property name="position">300</property> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <child> + <object class="DinoUiConversationSelectorView" id="conversation_list"> + <property name="visible">True</property> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkOverlay"> + <property name="visible">True</property> + <child> + <object class="GtkOverlay"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <style> + <class name="dino-conversation"/> + </style> + <child> + <object class="DinoUiConversationSummaryConversationView" id="conversation_frame"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="DinoUiChatInputView" id="chat_input"> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child type="overlay"> + <object class="GtkRevealer" id="goto_end_revealer"> + <property name="halign">end</property> + <property name="valign">end</property> + <property name="transition-type">crossfade</property> + <property name="visible">True</property> + <property name="margin-end">30</property> + <property name="margin-bottom">70</property> + <child> + <object class="GtkButton" id="goto_end_button"> + <property name="vexpand">False</property> + <property name="halign">end</property> + <property name="valign">end</property> + <property name="visible">True</property> + <style> + <class name="circular"/> + </style> + <child> + <object class="GtkImage"> + <property name="icon-name">go-down-symbolic</property> + <property name="icon-size">1</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="overlay"> + <object class="GtkRevealer" id="search_revealer"> + <property name="visible">True</property> + <property name="halign">end</property> + <property name="transition-type">slide-left</property> + <style> + <class name="dino-sidebar"/> + </style> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="width-request">400</property> + <property name="shadow-type">none</property> + <child> + <object class="DinoUiGlobalSearch" id="search_box"> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">False</property> + </packing> + </child> + </object> +</interface> diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 22d6d93d..86a4e288 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -32,7 +32,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { window = new UnifiedWindow(this, stream_interactor); notifications = new Notifications(stream_interactor, window); notifications.start(); - notifications.conversation_selected.connect(window.on_conversation_selected); + notifications.conversation_selected.connect((conversation) => window.on_conversation_selected(conversation)); } window.present(); }); diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index a1c2b83d..dd111997 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -32,7 +32,7 @@ public class View : Box { [GtkChild] private Separator file_separator; private EncryptionButton encryption_widget = new EncryptionButton() { margin_top=3, valign=Align.START, visible=true }; - public View(StreamInteractor stream_interactor) { + public View init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input); @@ -70,6 +70,7 @@ public class View : Box { Util.force_css(frame, "* { border-radius: 3px; }"); stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available); + return this; } public void initialize_for_conversation(Conversation conversation) { diff --git a/main/src/ui/conversation_list_titlebar.vala b/main/src/ui/conversation_list_titlebar.vala index 65515019..60d9a6fb 100644 --- a/main/src/ui/conversation_list_titlebar.vala +++ b/main/src/ui/conversation_list_titlebar.vala @@ -10,7 +10,6 @@ public class ConversationListTitlebar : Gtk.HeaderBar { public signal void conversation_opened(Conversation conversation); [GtkChild] private MenuButton add_button; - [GtkChild] public ToggleButton search_button; private StreamInteractor stream_interactor; diff --git a/main/src/ui/conversation_selector/view.vala b/main/src/ui/conversation_selector/view.vala index b6b02848..d06ad133 100644 --- a/main/src/ui/conversation_selector/view.vala +++ b/main/src/ui/conversation_selector/view.vala @@ -10,43 +10,14 @@ namespace Dino.Ui.ConversationSelector { public class View : Box { public List conversation_list; - [GtkChild] public SearchEntry search_entry; - [GtkChild] public Revealer search_revealer; [GtkChild] private ScrolledWindow scrolled; - public View(StreamInteractor stream_interactor) { + public View init(StreamInteractor stream_interactor) { conversation_list = new List(stream_interactor) { visible=true }; scrolled.add(conversation_list); - search_entry.key_release_event.connect(search_key_release_event); - search_entry.search_changed.connect(search_changed); + return this; } - public void conversation_selected(Conversation? conversation) { - search_entry.set_text(""); - } - - private void refilter() { - string[]? values = null; - string str = search_entry.get_text (); - if (str != "") values = str.split(" "); - conversation_list.set_filter_values(values); - } - - private void search_changed(Editable editable) { - refilter(); - } - - private bool search_key_release_event(EventKey event) { - conversation_list.select_row(conversation_list.get_row_at_y(0)); - if (event.keyval == Key.Down) { - ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0); - if (row != null) { - conversation_list.select_row(row); - row.grab_focus(); - } - } - return false; - } } } diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala index 1ea52a6d..d07ab743 100644 --- a/main/src/ui/conversation_summary/chat_state_populator.vala +++ b/main/src/ui/conversation_summary/chat_state_populator.vala @@ -6,7 +6,7 @@ using Xmpp; namespace Dino.Ui.ConversationSummary { -class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { +class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { public string id { get { return "chat_state"; } } @@ -43,8 +43,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } - public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } - private void update_chat_state(Account account, Jid jid) { HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation); diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala new file mode 100644 index 00000000..8a2bf136 --- /dev/null +++ b/main/src/ui/conversation_summary/content_item_widget_factory.vala @@ -0,0 +1,224 @@ +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class ContentItemWidgetFactory : Object { + + private StreamInteractor stream_interactor; + private HashMap<string, WidgetGenerator> generators = new HashMap<string, WidgetGenerator>(); + + public ContentItemWidgetFactory(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor); + generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor); + } + + public Widget? get_widget(ContentItem item) { + WidgetGenerator? generator = generators[item.type_]; + if (generator != null) { + return (Widget?) generator.get_widget(item); + } + return null; + } + + public void register_widget_generator(WidgetGenerator generator) { + generators[generator.handles_type] = generator; + } +} + +public interface WidgetGenerator : Object { + public abstract string handles_type { get; set; } + public abstract Object get_widget(ContentItem item); +} + +public class MessageItemWidgetGenerator : WidgetGenerator, Object { + + public string handles_type { get; set; default=FileItem.TYPE; } + + private StreamInteractor stream_interactor; + + public MessageItemWidgetGenerator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public Object get_widget(ContentItem item) { + MessageItem message_item = item as MessageItem; + Conversation conversation = message_item.conversation; + Message message = message_item.message; + + MessageTextView text_view = new MessageTextView() { vexpand=true, visible = true }; + + if (message_item.message.body.has_prefix("/me")) { + text_view.add_text(message.body.substring(3)); + } else { + text_view.add_text(message.body); + } + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + text_view.highlight_word(conversation.nickname); + } + if (message_item.message.body.has_prefix("/me")) { + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view)); + TextTag nick_tag = text_view.buffer.create_tag(null, foreground: @"#$color"); + TextIter iter; + text_view.buffer.get_start_iter(out iter); + text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag); + + text_view.style_updated.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view)); + text_view.realize.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view)); + } + return text_view; + } + + public static void update_style(StreamInteractor stream_interactor, Message message, Conversation conversation, TextTag nick_tag, TextView text_view) { + string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view)); + nick_tag.foreground = "#" + color; + } +} + +public class FileItemWidgetGenerator : WidgetGenerator, Object { + + public StreamInteractor stream_interactor; + public string handles_type { get; set; default=FileItem.TYPE; } + + private const int MAX_HEIGHT = 300; + private const int MAX_WIDTH = 600; + + public FileItemWidgetGenerator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public Object get_widget(ContentItem item) { + FileItem file_item = item as FileItem; + FileTransfer transfer = file_item.file_transfer; + if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) { + return getImageWidget(transfer); + } else { + return getDefaultWidget(transfer); + } + } + + private Widget getImageWidget(FileTransfer file_transfer) { + Image image = new Image() { halign=Align.START, visible = true }; + Gdk.Pixbuf pixbuf; + try { + pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); + } catch (Error error) { + return null; + } + + int max_scaled_height = MAX_HEIGHT * image.scale_factor; + if (pixbuf.height > max_scaled_height) { + pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); + } + int max_scaled_width = MAX_WIDTH * image.scale_factor; + if (pixbuf.width > max_scaled_width) { + pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); + } + pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); + Util.image_set_from_scaled_pixbuf(image, pixbuf); + Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); + + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui"); + Widget toolbar = builder.get_object("main") as Widget; + Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); + Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); + + Label url_label = builder.get_object("url_label") as Label; + Util.force_color(url_label, "#eee"); + + if (file_transfer.file_name != null && file_transfer.file_name != "") { + string caption = file_transfer.file_name; + url_label.label = caption; + } else { + url_label.visible = false; + } + + Image open_image = builder.get_object("open_image") as Image; + Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); + Button open_button = builder.get_object("open_button") as Button; + Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); + open_button.clicked.connect(() => { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n"); + } + }); + + Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; + toolbar_revealer.add(toolbar); + + Grid grid = new Grid() { visible=true }; + grid.attach(toolbar_revealer, 0, 0, 1, 1); + grid.attach(image, 0, 0, 1, 1); + + EventBox event_box = new EventBox() { halign=Align.START, visible=true }; + event_box.add(grid); + event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); + event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); + + return event_box; + } + + private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { + Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); + Gdk.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 Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private Widget getDefaultWidget(FileTransfer file_transfer) { + Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true }; + string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type); + Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true }; + main_box.add(content_type_image); + + Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true }; + Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true}; + right_box.add(name_label); + Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true}; + mime_label.get_style_context().add_class("dim-label"); + right_box.add(mime_label); + main_box.add(right_box); + + EventBox event_box = new EventBox() { halign=Align.START, visible=true }; + event_box.add(main_box); + + event_box.enter_notify_event.connect((event) => { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); + return false; + }); + event_box.leave_notify_event.connect((event) => { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); + return false; + }); + event_box.button_release_event.connect((event_button) => { + if (event_button.button == 1) { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + print("Tried to open " + file_transfer.get_file().get_path()); + } + } + return false; + }); + + return event_box; + } +} + +} diff --git a/main/src/ui/conversation_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala new file mode 100644 index 00000000..9ebb9159 --- /dev/null +++ b/main/src/ui/conversation_summary/content_populator.vala @@ -0,0 +1,110 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class ContentProvider : ContentItemCollection, Object { + + private StreamInteractor stream_interactor; + private ContentItemWidgetFactory widget_factory; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + + public ContentProvider(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + this.widget_factory = new ContentItemWidgetFactory(stream_interactor); + } + + public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) { + if (current_conversation != null) { + stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this); + } + current_conversation = conversation; + this.item_collection = item_collection; + stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this); + } + + public void insert_item(ContentItem item) { + item_collection.insert_item(new ContentMetaItem(item, widget_factory)); + } + + public void remove_item(ContentItem item) { } + + + public Gee.List<ContentMetaItem> populate_latest(Conversation conversation, int n) { + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation, n); + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public Gee.List<ContentMetaItem> populate_before(Conversation conversation, ContentItem before_item, int n) { + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public Gee.List<ContentMetaItem> populate_after(Conversation conversation, ContentItem after_item, int n) { + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public ContentMetaItem get_content_meta_item(ContentItem content_item) { + return new ContentMetaItem(content_item, widget_factory); + } +} + +public class ContentMetaItem : Plugins.MetaConversationItem { + public override Jid? jid { get; set; } + public override DateTime? sort_time { get; set; } + public override DateTime? display_time { get; set; } + public override Encryption? encryption { get; set; } + + public ContentItem content_item; + private ContentItemWidgetFactory widget_factory; + + public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { + this.jid = content_item.jid; + this.sort_time = content_item.sort_time; + this.seccondary_sort_indicator = content_item.seccondary_sort_indicator; + this.display_time = content_item.display_time; + this.encryption = content_item.encryption; + this.mark = content_item.mark; + + WeakRef weak_item = WeakRef(content_item); + content_item.notify["mark"].connect(() => { + ContentItem? ci = weak_item.get() as ContentItem; + if (ci == null) return; + this.mark = ci.mark; + }); + + this.can_merge = true; + this.requires_avatar = true; + this.requires_header = true; + + this.content_item = content_item; + this.widget_factory = widget_factory; + } + + public override bool can_merge { get; set; default=true; } + public override bool requires_avatar { get; set; default=true; } + public override bool requires_header { get; set; default=true; } + + public override Object? get_widget(Plugins.WidgetType type) { + return widget_factory.get_widget(content_item); + } +} + +} diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala index a8da93ef..a4e45f7a 100644 --- a/main/src/ui/conversation_summary/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala @@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box { return datetime.format(format); } - public virtual string get_relative_time(DateTime datetime) { + public static string get_relative_time(DateTime datetime) { DateTime now = new DateTime.now_local(); TimeSpan timespan = now.difference(datetime); if (timespan > 365 * TimeSpan.DAY) { diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index fac53b7d..e6a564de 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -11,19 +11,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { public Conversation? conversation { get; private set; } - [GtkChild] private ScrolledWindow scrolled; + [GtkChild] public ScrolledWindow scrolled; [GtkChild] private Revealer notification_revealer; [GtkChild] private Box notifications; [GtkChild] private Box main; [GtkChild] private Stack stack; private StreamInteractor stream_interactor; - private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items); - private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>(); + private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items); + private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items); private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>(); private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>(); private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>(); - private MessagePopulator message_item_populator; + private ContentProvider content_populator; private SubscriptionNotitication subscription_notification; private double? was_value; @@ -33,22 +33,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private Mutex reloading_mutex = Mutex(); private bool animate = false; private bool firstLoad = true; + private bool at_current_content = true; + private bool reload_messages = true; - public ConversationView(StreamInteractor stream_interactor) { + public ConversationView init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); scrolled.vadjustment.notify["value"].connect(on_value_notify); - message_item_populator = new MessagePopulator(stream_interactor); + content_populator = new ContentProvider(stream_interactor); subscription_notification = new SubscriptionNotitication(stream_interactor); - insert_item.connect(on_insert_item); - remove_item.connect(on_remove_item); + insert_item.connect(filter_insert_item); + remove_item.connect(do_remove_item); Application app = GLib.Application.get_default() as Application; - app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor)); - app.plugin_registry.register_conversation_item_populator(new FilePopulator(stream_interactor)); - app.plugin_registry.register_conversation_item_populator(new DateSeparatorPopulator(stream_interactor)); + app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); + app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor)); Timeout.add_seconds(60, () => { foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { @@ -57,7 +58,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { return true; }); - Util.force_base_background(this); + return this; } // Workaround GTK TextView issues: Delay first load of contents @@ -65,54 +66,127 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { if (firstLoad) { int timeout = firstLoad ? 1000 : 0; Timeout.add(timeout, () => { + stack.set_visible_child_name("void"); initialize_for_conversation_(conversation); + display_latest(); + stack.set_visible_child_name("main"); return false; }); firstLoad = false; } else { + stack.set_visible_child_name("void"); initialize_for_conversation_(conversation); + display_latest(); + stack.set_visible_child_name("main"); } + } + public void initialize_around_message(Conversation conversation, ContentItem content_item) { + stack.set_visible_child_name("void"); + clear(); + initialize_for_conversation_(conversation); + Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40); + foreach (ContentMetaItem item in before_items) { + do_insert_item(item); + } + ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); + meta_item.can_merge = false; + Widget w = insert_new(meta_item); + content_items.add(meta_item); + meta_items.add(meta_item); + + Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40); + foreach (ContentMetaItem item in after_items) { + do_insert_item(item); + } + if (after_items.size == 40) { + at_current_content = false; + } + { + int h = 0, i = 0; + main.@foreach((widget) => { + if (i >= before_items.size) return; + ConversationItemSkeleton? sk = widget as ConversationItemSkeleton; + i += sk != null ? sk.items.size : 1; + int minimum_height, natural_height; + widget.get_preferred_height_for_width(main.get_allocated_width() - 2 * main.margin, out minimum_height, out natural_height); + h += minimum_height + 15; + }); + } + + reload_messages = false; + Timeout.add(700, () => { + int h = 0, i = 0; + main.@foreach((widget) => { + if (i >= before_items.size) return; + ConversationItemSkeleton? sk = widget as ConversationItemSkeleton; + i += sk != null ? sk.items.size : 1; + h += widget.get_allocated_height() + 15; + }); + scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; + w.get_style_context().add_class("highlight-once"); + reload_messages = true; + stack.set_visible_child_name("main"); + return false; + }); } private void initialize_for_conversation_(Conversation? conversation) { Dino.Application app = Dino.Application.get_default(); if (this.conversation != null) { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.close(conversation); } } this.conversation = conversation; - stack.set_visible_child_name("void"); - clear(); - was_upper = null; - was_page_size = null; + + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.init(conversation, this, Plugins.WidgetType.GTK); + } + content_populator.init(this, conversation, Plugins.WidgetType.GTK); + subscription_notification.init(conversation, this); + animate = false; Timeout.add(20, () => { animate = true; return false; }); + } - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); + private void display_latest() { + clear(); + + Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40); + foreach (ContentMetaItem item in items) { + do_insert_item(item); } - message_item_populator.init(conversation, this); - message_item_populator.populate_latest(conversation, 40); Idle.add(() => { on_value_notify(); return false; }); + } - subscription_notification.init(conversation, this); - - stack.set_visible_child_name("main"); + public void filter_insert_item(Plugins.MetaConversationItem item) { + if (meta_items.size > 0) { + bool after_last = meta_items.last().sort_time.compare(item.sort_time) < 0; + bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0; + bool accept = within_range || (at_current_content && after_last); + if (!accept) { + return; + } + } + do_insert_item(item); } - public void on_insert_item(Plugins.MetaConversationItem item) { + public void do_insert_item(Plugins.MetaConversationItem item) { lock (meta_items) { if (!item.can_merge || !merge_back(item)) { insert_new(item); } } + if (item as ContentMetaItem != null) { + content_items.add(item); + } + meta_items.add(item); } - public void on_remove_item(Plugins.MetaConversationItem item) { - lock (meta_items) { - ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + private void do_remove_item(Plugins.MetaConversationItem item) { + ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + if (skeleton != null) { if (skeleton.items.size > 1) { skeleton.remove_meta_item(item); } else { @@ -122,6 +196,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { item_skeletons.remove(skeleton); item_item_skeletons.unset(item); } + content_items.remove(item); meta_items.remove(item); } } @@ -151,10 +226,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { lower_start_item.encryption == item.encryption && (item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) { lower_skeleton.add_meta_item(item); - force_alloc_width(lower_skeleton, main.get_allocated_width()); + Util.force_alloc_width(lower_skeleton, main.get_allocated_width()); + widgets[item] = widgets[lower_start_item]; item_item_skeletons[item] = lower_skeleton; - meta_items.add(item); return true; } @@ -162,7 +237,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { return false; } - private void insert_new(Plugins.MetaConversationItem item) { + private Widget insert_new(Plugins.MetaConversationItem item) { Plugins.MetaConversationItem? lower_item = meta_items.lower(item); // Does another skeleton need to be split? @@ -181,7 +256,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { item_item_skeletons[item] = item_skeleton; int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; item_skeletons.insert(index, item_skeleton); - meta_items.add(item); // Insert widget Widget insert = item_skeleton; @@ -195,22 +269,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { main.add(insert); } widgets[item] = insert; - force_alloc_width(insert, main.get_allocated_width()); + Util.force_alloc_width(insert, main.get_allocated_width()); main.reorder_child(insert, index); // If an item from the past was added, add everything between that item and the (post-)first present item if (index == 0) { Dino.Application app = Dino.Application.get_default(); if (item_skeletons.size == 1) { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc()); } } else { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time); } } } + return insert; } private void split_at_time(ConversationItemSkeleton split_skeleton, DateTime time) { @@ -219,12 +294,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { while(i < split_skeleton.items.size) { Plugins.MetaConversationItem meta_item = split_skeleton.items[i]; if (time.compare(meta_item.display_time) < 0) { - remove_item(meta_item); + do_remove_item(meta_item); if (!already_divided) { insert_new(meta_item); already_divided = true; } else { - insert_item(meta_item); + do_insert_item(meta_item); } } i++; @@ -232,51 +307,73 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } 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 + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + if (at_current_content) { + 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; + was_value = scrolled.vadjustment.value; reloading_mutex.trylock(); reloading_mutex.unlock(); } private void on_value_notify() { - if (scrolled.vadjustment.value < 200) { + if (scrolled.vadjustment.value < 400) { load_earlier_messages(); + } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) { + load_later_messages(); } } private void load_earlier_messages() { was_value = scrolled.vadjustment.value; if (!reloading_mutex.trylock()) return; - if (meta_items.size > 0) message_item_populator.populate_before(conversation, meta_items.first(), 20); + if (meta_items.size > 0) { + Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } } - private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { + private void load_later_messages() { + if (!reloading_mutex.trylock()) return; + if (meta_items.size > 0 && !at_current_content) { + Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); + if (items.size == 0) { + at_current_content = true; + } + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } + } + + private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { int res = a.sort_time.compare(b.sort_time); if (res == 0) { - if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1; - else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) res = 1; + if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) { + res = -1; + } else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) { + res = 1; + } } return res; } - // Workaround GTK TextView issues - 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() { + was_upper = null; + was_page_size = null; + content_items.clear(); meta_items.clear(); - meta_after_items.clear(); item_skeletons.clear(); item_item_skeletons.clear(); widgets.clear(); diff --git a/main/src/ui/conversation_summary/date_separator_populator.vala b/main/src/ui/conversation_summary/date_separator_populator.vala index 34005ab6..6a1ba782 100644 --- a/main/src/ui/conversation_summary/date_separator_populator.vala +++ b/main/src/ui/conversation_summary/date_separator_populator.vala @@ -6,7 +6,7 @@ using Xmpp; namespace Dino.Ui.ConversationSummary { -class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object { +class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { public string id { get { return "date_separator"; } } @@ -35,8 +35,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object { public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } - public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } - private void on_insert_item(Plugins.MetaConversationItem item) { if (item.display_time == null) return; diff --git a/main/src/ui/conversation_summary/default_file_display.vala b/main/src/ui/conversation_summary/default_file_display.vala deleted file mode 100644 index 1547440b..00000000 --- a/main/src/ui/conversation_summary/default_file_display.vala +++ /dev/null @@ -1,95 +0,0 @@ -using Gdk; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class DefaultFileDisplay : Plugins.MetaConversationItem { - public override Jid? jid { get; set; } - public override DateTime? sort_time { get; set; } - public override DateTime? display_time { get; set; } - public override Encryption? encryption { get; set; } - public override Entities.Message.Marked? mark { get; set; } - - public override bool can_merge { get; set; default=true; } - public override bool requires_avatar { get; set; default=true; } - public override bool requires_header { get; set; default=true; } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - private StreamInteractor stream_interactor; - private FileTransfer file_transfer; - - public DefaultFileDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; - this.file_transfer = file_transfer; - - this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; - this.sort_time = file_transfer.time; - this.seccondary_sort_indicator = file_transfer.id + 0.2903; - this.display_time = file_transfer.time; - this.encryption = file_transfer.encryption; - this.mark = file_to_message_state(file_transfer.state); - file_transfer.notify["state"].connect_after(() => { - this.mark = file_to_message_state(file_transfer.state); - }); - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true }; - string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type); - Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true }; - main_box.add(content_type_image); - - Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true }; - Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true}; - right_box.add(name_label); - Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true}; - mime_label.get_style_context().add_class("dim-label"); - right_box.add(mime_label); - main_box.add(right_box); - - EventBox event_box = new EventBox() { halign=Align.START, visible=true }; - event_box.add(main_box); - - event_box.enter_notify_event.connect((event) => { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); - return false; - }); - event_box.leave_notify_event.connect((event) => { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); - return false; - }); - event_box.button_release_event.connect((event_button) => { - if (event_button.button == 1) { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - print("Tried to open " + file_transfer.get_file().get_path()); - } - } - return false; - }); - - return event_box; - } - - private Entities.Message.Marked file_to_message_state(FileTransfer.State state) { - switch (state) { - case FileTransfer.State.IN_PROCESS: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.COMPLETE: - return Entities.Message.Marked.NONE; - case FileTransfer.State.NOT_STARTED: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.FAILED: - return Entities.Message.Marked.WONTSEND; - } - assert_not_reached(); - } -} - -} diff --git a/main/src/ui/conversation_summary/default_message_display.vala b/main/src/ui/conversation_summary/default_message_display.vala deleted file mode 100644 index 519e5107..00000000 --- a/main/src/ui/conversation_summary/default_message_display.vala +++ /dev/null @@ -1,58 +0,0 @@ -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class DefaultMessageDisplay : Plugins.MessageDisplayProvider, Object { - public string id { get; set; default="default"; } - public double priority { get; set; default=0; } - - public StreamInteractor stream_interactor; - - public DefaultMessageDisplay(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - } - - public bool can_display(Entities.Message? message) { return true; } - - public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) { - return new MetaMessageItem(stream_interactor, message, conversation); - } -} - -public class MetaMessageItem : Plugins.MetaConversationItem { - public override Jid? jid { get; set; } - public override DateTime? sort_time { get; set; } - public override DateTime? display_time { get; set; } - public override Encryption? encryption { get; set; } - - private StreamInteractor stream_interactor; - private Conversation conversation; - private Message message; - - public MetaMessageItem(StreamInteractor stream_interactor, Message message, Conversation conversation) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.message = message; - this.jid = message.from; - this.sort_time = message.local_time; - this.seccondary_sort_indicator = message.id + 0.2085; - this.display_time = message.time; - this.encryption = message.encryption; - } - - public override bool can_merge { get; set; default=true; } - public override bool requires_avatar { get; set; default=true; } - public override bool requires_header { get; set; default=true; } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - MessageTextView text_view = new MessageTextView() { visible = true }; - text_view.add_text(message.body); - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - text_view.highlight_word(conversation.nickname); - } - return text_view; - } -} - -} diff --git a/main/src/ui/conversation_summary/file_populator.vala b/main/src/ui/conversation_summary/file_populator.vala deleted file mode 100644 index af7bc992..00000000 --- a/main/src/ui/conversation_summary/file_populator.vala +++ /dev/null @@ -1,54 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -class FilePopulator : Plugins.ConversationItemPopulator, Object { - - public string id { get { return "file"; } } - - private StreamInteractor? stream_interactor; - private Conversation? current_conversation; - private Plugins.ConversationItemCollection? item_collection; - - public FilePopulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.get_module(FileManager.IDENTITY).received_file.connect((file_transfer) => { - if (current_conversation != null && current_conversation.account.equals(file_transfer.account) && current_conversation.counterpart.equals_bare(file_transfer.counterpart)) { - insert_file(file_transfer); - } - }); - } - - public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { - current_conversation = conversation; - this.item_collection = item_collection; - } - - public void close(Conversation conversation) { } - - public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { - Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_file_transfers(conversation.account, conversation.counterpart, from, to); - foreach (FileTransfer transfer in transfers) { - insert_file(transfer); - } - } - - public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } - - private void insert_file(FileTransfer transfer) { - Plugins.MetaConversationItem item = null; - if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) { - item = new ImageDisplay(stream_interactor, transfer); - } else { - item = new DefaultFileDisplay(stream_interactor, transfer); - } - item_collection.insert_item(item); - } -} - -} diff --git a/main/src/ui/conversation_summary/image_display.vala b/main/src/ui/conversation_summary/image_display.vala deleted file mode 100644 index 15880836..00000000 --- a/main/src/ui/conversation_summary/image_display.vala +++ /dev/null @@ -1,137 +0,0 @@ -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class ImageDisplay : Plugins.MetaConversationItem { - public override Jid? jid { get; set; } - public override DateTime? sort_time { get; set; } - public override DateTime? display_time { get; set; } - public override Encryption? encryption { get; set; } - public override Entities.Message.Marked? mark { get; set; } - - public override bool can_merge { get; set; default=true; } - public override bool requires_avatar { get; set; default=true; } - public override bool requires_header { get; set; default=true; } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - private StreamInteractor stream_interactor; - private FileTransfer file_transfer; - - public ImageDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; - this.file_transfer = file_transfer; - - this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; - this.sort_time = file_transfer.time; - this.seccondary_sort_indicator = file_transfer.id + 0.2903; - this.display_time = file_transfer.time; - this.encryption = file_transfer.encryption; - this.mark = file_to_message_state(file_transfer.state); - file_transfer.notify["state"].connect_after(() => { - this.mark = file_to_message_state(file_transfer.state); - }); - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - Image image = new Image() { halign=Align.START, visible = true }; - Gdk.Pixbuf pixbuf; - try { - pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); - } catch (Error error) { - return null; - } - - int max_scaled_height = MAX_HEIGHT * image.scale_factor; - if (pixbuf.height > max_scaled_height) { - pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); - } - int max_scaled_width = MAX_WIDTH * image.scale_factor; - if (pixbuf.width > max_scaled_width) { - pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); - } - pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); - Util.image_set_from_scaled_pixbuf(image, pixbuf); - Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); - - Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui"); - Widget toolbar = builder.get_object("main") as Widget; - Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); - Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); - - Label url_label = builder.get_object("url_label") as Label; - Util.force_color(url_label, "#eee"); - update_info(url_label, file_transfer.file_name); - - Image open_image = builder.get_object("open_image") as Image; - Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); - Button open_button = builder.get_object("open_button") as Button; - Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); - open_button.clicked.connect(() => { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n"); - } - }); - - Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; - toolbar_revealer.add(toolbar); - - Grid grid = new Grid() { visible=true }; - grid.attach(toolbar_revealer, 0, 0, 1, 1); - grid.attach(image, 0, 0, 1, 1); - - EventBox event_box = new EventBox() { halign=Align.START, visible=true }; - event_box.add(grid); - event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); - event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); - - return event_box; - } - - private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { - Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); - Gdk.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 Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); - } - - private void update_info(Label url_label, string? info) { - string url = info ?? ""; - if (url.has_prefix("https://")) url = url.substring(8); - if (url.has_prefix("http://")) url = url.substring(7); - if (url.has_prefix("www.")) url = url.substring(4); - string[] slash_split = url.split("/"); - if (slash_split.length > 2) url = slash_split[0] + "/…/" + slash_split[slash_split.length - 1]; - url_label.label = url; - } - - private Entities.Message.Marked file_to_message_state(FileTransfer.State state) { - switch (state) { - case FileTransfer.State.IN_PROCESS: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.COMPLETE: - return Entities.Message.Marked.NONE; - case FileTransfer.State.NOT_STARTED: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.FAILED: - return Entities.Message.Marked.WONTSEND; - } - assert_not_reached(); - } -} - -} diff --git a/main/src/ui/conversation_summary/message_populator.vala b/main/src/ui/conversation_summary/message_populator.vala deleted file mode 100644 index b342306b..00000000 --- a/main/src/ui/conversation_summary/message_populator.vala +++ /dev/null @@ -1,81 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class MessagePopulator : Object { - - private StreamInteractor? stream_interactor; - private Conversation? current_conversation; - private Plugins.ConversationItemCollection? item_collection; - private HashMap<Plugins.MetaConversationItem, Message> meta_message = new HashMap<Plugins.MetaConversationItem, Message>(); - - public MessagePopulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - Application app = GLib.Application.get_default() as Application; - app.plugin_registry.register_message_display(new DefaultMessageDisplay(stream_interactor)); - app.plugin_registry.register_message_display(new SlashmeMessageDisplay(stream_interactor)); - - - stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(handle_message); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_message); - } - - public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection) { - current_conversation = conversation; - this.item_collection = item_collection; - } - - public void close(Conversation conversation) { } - - public void populate_latest(Conversation conversation, int n) { - Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation, n); - if (messages != null) { - foreach (Entities.Message message in messages) { - handle_message(message, conversation); - } - } - } - - public void populate_before(Conversation conversation, Plugins.MetaConversationItem item, int n) { - Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, meta_message[item], n); - if (messages != null) { - foreach (Entities.Message message in messages) { - handle_message(message, conversation); - } - } - } - - private void handle_message(Message message, Conversation conversation) { - if (!conversation.equals(current_conversation)) return; - - Plugins.MessageDisplayProvider? best_provider = null; - double priority = -1; - Application app = GLib.Application.get_default() as Application; - foreach (Plugins.MessageDisplayProvider provider in app.plugin_registry.message_displays) { - if (provider.can_display(message) && provider.priority > priority) { - best_provider = provider; - priority = provider.priority; - } - } - Plugins.MetaConversationItem? meta_item = best_provider.get_item(message, conversation); - if (meta_item == null) return; - meta_message[meta_item] = message; - - meta_item.mark = message.marked; - WeakRef weak_meta_item = WeakRef(meta_item); - WeakRef weak_message = WeakRef(message); - message.notify["marked"].connect(() => { - Plugins.MetaConversationItem? mi = weak_meta_item.get() as Plugins.MetaConversationItem; - Message? m = weak_message.get() as Message; - if (mi == null || m == null) return; - mi.mark = m.marked; - }); - item_collection.insert_item(meta_item); - } -} - -} diff --git a/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala index 0b5ed6e4..71ca35f8 100644 --- a/main/src/ui/conversation_summary/message_textview.vala +++ b/main/src/ui/conversation_summary/message_textview.vala @@ -24,7 +24,6 @@ public class MessageTextView : TextView { motion_notify_event.connect(change_cursor_over_url); update_display_style(); - Util.force_base_background(this, "textview, text:not(:selected)"); style_updated.connect(update_display_style); populate_popup.connect(populate_context_menu); } @@ -60,7 +59,7 @@ public class MessageTextView : TextView { TextIter end_iter; buffer.get_iter_at_offset(out start_iter, start); buffer.get_iter_at_offset(out end_iter, end); - buffer.apply_tag_by_name("semibold", start_iter, end_iter); + buffer.apply_tag(bold_tag, start_iter, end_iter); } } @@ -125,7 +124,7 @@ public class MessageTextView : TextView { TextIter end_iter; buffer.get_iter_at_offset(out start_iter, absolute_start + start); buffer.get_iter_at_offset(out end_iter, absolute_start + end); - buffer.apply_tag_by_name("url", start_iter, end_iter); + buffer.apply_tag(link_tag, start_iter, end_iter); } } diff --git a/main/src/ui/conversation_summary/slashme_message_display.vala b/main/src/ui/conversation_summary/slashme_message_display.vala deleted file mode 100644 index 1ee20748..00000000 --- a/main/src/ui/conversation_summary/slashme_message_display.vala +++ /dev/null @@ -1,79 +0,0 @@ -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class SlashmeMessageDisplay : Plugins.MessageDisplayProvider, Object { - public string id { get; set; default="slashme"; } - public double priority { get; set; default=1; } - - public StreamInteractor stream_interactor; - - public SlashmeMessageDisplay(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - } - - public bool can_display(Entities.Message? message) { - return message.body.has_prefix("/me"); - } - - public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) { - return new MetaSlashmeItem(stream_interactor, message, conversation); - } -} - -public class MetaSlashmeItem : Plugins.MetaConversationItem { - public override Jid? jid { get; set; } - public override DateTime? sort_time { get; set; } - public override DateTime? display_time { get; set; } - public override Encryption? encryption { get; set; } - - private StreamInteractor stream_interactor; - private Conversation conversation; - private Message message; - private TextTag nick_tag; - private MessageTextView text_view; - - public MetaSlashmeItem(StreamInteractor stream_interactor, Message message, Conversation conversation) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.message = message; - this.jid = message.from; - this.sort_time = message.local_time; - this.seccondary_sort_indicator = message.id + 0.0845; - this.display_time = message.time; - this.encryption = message.encryption; - } - - public override bool can_merge { get; set; default=false; } - public override bool requires_avatar { get; set; default=true; } - public override bool requires_header { get; set; default=false; } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - text_view = new MessageTextView() { valign=Align.CENTER, vexpand=true, visible = true }; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - text_view.highlight_word(conversation.nickname); - } - - string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); - string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view)); - nick_tag = text_view.buffer.create_tag("nick", foreground: "#" + color); - TextIter iter; - text_view.buffer.get_start_iter(out iter); - text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag); - text_view.add_text(message.body.substring(3)); - - text_view.style_updated.connect(update_style); - text_view.realize.connect(update_style); - return text_view; - } - - private void update_style() { - string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view)); - nick_tag.foreground = "#" + color; - } -} - -} diff --git a/main/src/ui/conversation_titlebar/search_entry.vala b/main/src/ui/conversation_titlebar/search_entry.vala new file mode 100644 index 00000000..b452bdce --- /dev/null +++ b/main/src/ui/conversation_titlebar/search_entry.vala @@ -0,0 +1,30 @@ +using Gtk; +using Gee; + +using Dino.Entities; + +namespace Dino.Ui { + +public class SearchMenuEntry : Plugins.ConversationTitlebarEntry, Object { + public string id { get { return "search"; } } + + Plugins.ConversationTitlebarWidget search_button; + + public SearchMenuEntry(Plugins.ConversationTitlebarWidget search_button) { + this.search_button = search_button; + } + + public double order { get { return 1; } } + public Plugins.ConversationTitlebarWidget? get_widget(Plugins.WidgetType type) { + if (type == Plugins.WidgetType.GTK) { + return search_button; + } + return null; + } +} + +public class GlobalSearchButton : Plugins.ConversationTitlebarWidget, Gtk.ToggleButton { + public new void set_conversation(Conversation conversation) { } +} + +} diff --git a/main/src/ui/conversation_titlebar/view.vala b/main/src/ui/conversation_titlebar/view.vala index d01cd9bb..13a9bf80 100644 --- a/main/src/ui/conversation_titlebar/view.vala +++ b/main/src/ui/conversation_titlebar/view.vala @@ -11,6 +11,7 @@ public class ConversationTitlebar : Gtk.HeaderBar { private Window window; private Conversation? conversation; private Gee.List<Plugins.ConversationTitlebarWidget> widgets = new ArrayList<Plugins.ConversationTitlebarWidget>(); + public GlobalSearchButton search_button = new GlobalSearchButton() { visible = true }; public ConversationTitlebar(StreamInteractor stream_interactor, Window window) { this.stream_interactor = stream_interactor; @@ -19,9 +20,11 @@ public class ConversationTitlebar : Gtk.HeaderBar { this.get_style_context().add_class("dino-right"); show_close_button = true; hexpand = true; + search_button.set_image(new Gtk.Image.from_icon_name("system-search-symbolic", Gtk.IconSize.MENU) { visible = true }); Application app = GLib.Application.get_default() as Application; app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor)); + app.plugin_registry.register_contact_titlebar_entry(new SearchMenuEntry(search_button)); app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window)); foreach(var e in app.plugin_registry.conversation_titlebar_entries) { diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala new file mode 100644 index 00000000..eadf142c --- /dev/null +++ b/main/src/ui/global_search.vala @@ -0,0 +1,259 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")] +class GlobalSearch : Overlay { + public signal void selected_item(MessageItem item); + private StreamInteractor stream_interactor; + private string search = ""; + private int loaded_results = -1; + private Mutex reloading_mutex = Mutex(); + + [GtkChild] public SearchEntry search_entry; + [GtkChild] public Label entry_number_label; + [GtkChild] public ScrolledWindow results_scrolled; + [GtkChild] public Box results_box; + [GtkChild] public Stack results_empty_stack; + [GtkChild] public Frame auto_complete_overlay; + [GtkChild] public ListBox auto_complete_list; + + public GlobalSearch init(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + search_entry.search_changed.connect(() => { + set_search(search_entry.text); + }); + search_entry.notify["text"].connect_after(() => { update_auto_complete(); }); + search_entry.notify["cursor-position"].connect_after(() => { update_auto_complete(); }); + + results_scrolled.vadjustment.notify["value"].connect(() => { + if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) { + if (!reloading_mutex.trylock()) return; + Gee.List<MessageItem> new_messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search, loaded_results); + if (new_messages.size == 0) { + reloading_mutex.unlock(); + return; + } + loaded_results += new_messages.size; + append_messages(new_messages); + } + }); + results_scrolled.vadjustment.notify["upper"].connect_after(() => { + reloading_mutex.trylock(); + reloading_mutex.unlock(); + }); + + event.connect((event) => { + if (auto_complete_overlay.visible) { + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Up) { + var row = auto_complete_list.get_selected_row(); + var index = row == null ? -1 : row.get_index() - 1; + if (index == -1) index = (int)auto_complete_list.get_children().length() - 1; + auto_complete_list.select_row(auto_complete_list.get_row_at_index(index)); + return true; + } + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Down) { + var row = auto_complete_list.get_selected_row(); + var index = row == null ? 0 : row.get_index() + 1; + if (index == auto_complete_list.get_children().length()) index = 0; + auto_complete_list.select_row(auto_complete_list.get_row_at_index(index)); + return true; + } + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Tab || + event.type == Gdk.EventType.KEY_RELEASE && event.key.keyval == Gdk.Key.Return) { + auto_complete_list.get_selected_row().activate(); + return true; + } + } + // TODO: Handle cursor movement in results + // TODO: Direct all keystrokes to text input + return false; + }); + + return this; + } + + private void update_auto_complete() { + Gee.List<SearchSuggestion> suggestions = stream_interactor.get_module(SearchProcessor.IDENTITY).suggest_auto_complete(search_entry.text, search_entry.cursor_position); + auto_complete_overlay.visible = suggestions.size > 0; + if (suggestions.size > 0) { + auto_complete_list.@foreach((widget) => auto_complete_list.remove(widget)); + foreach(SearchSuggestion suggestion in suggestions) { + Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui"); + AvatarImage avatar = (AvatarImage)builder.get_object("image"); + avatar.set_jid(stream_interactor, suggestion.jid, suggestion.account); + Label label = (Label)builder.get_object("label"); + string display_name = Util.get_display_name(stream_interactor, suggestion.jid, suggestion.account); + if (display_name != suggestion.jid.to_string()) { + label.set_markup(@"$display_name <span font_weight='light' fgalpha='80%'>$(suggestion.jid)</span>"); + } else { + label.label = display_name; + } + ListBoxRow row = new ListBoxRow() { visible = true, can_focus = false }; + row.add((Widget)builder.get_object("root")); + row.activate.connect(() => { + handle_suggestion(suggestion); + }); + auto_complete_list.add(row); + } + auto_complete_list.select_row(auto_complete_list.get_row_at_index(0)); + } + } + + private void handle_suggestion(SearchSuggestion suggestion) { + search_entry.move_cursor(MovementStep.LOGICAL_POSITIONS, suggestion.start_index - search_entry.cursor_position, false); + search_entry.delete_from_cursor(DeleteType.CHARS, suggestion.end_index - suggestion.start_index); + search_entry.insert_at_cursor(suggestion.completion + " "); + } + + private void clear_search() { + results_box.@foreach((widget) => { widget.destroy(); }); + } + + private void set_search(string search) { + clear_search(); + this.search = search; + + if (get_keywords(search).is_empty) { + results_empty_stack.set_visible_child_name("empty"); + return; + } + + Gee.List<MessageItem> messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search); + if (messages.size == 0) { + results_empty_stack.set_visible_child_name("no-result"); + } else { + results_empty_stack.set_visible_child_name("results"); + + int match_count = messages.size < 10 ? messages.size : stream_interactor.get_module(SearchProcessor.IDENTITY).count_match_messages(search); + entry_number_label.label = "<i>" + _("%i search results").printf(match_count) + "</i>"; + loaded_results += messages.size; + append_messages(messages); + } + } + + private void append_messages(Gee.List<MessageItem> messages) { + foreach (MessageItem item in messages) { + Gee.List<MessageItem> before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(item.conversation, item.message.local_time, item.message.id, 1); + Gee.List<MessageItem> after_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(item.conversation, item.message.local_time, item.message.id, 1); + + Box context_box = new Box(Orientation.VERTICAL, 5) { visible=true }; + if (before_message != null && before_message.size > 0) { + context_box.add(get_context_message_widget(before_message.first())); + } + + Widget match_widget = get_match_message_widget(item); + Util.force_alloc_width(match_widget, results_empty_stack.get_allocated_width() - results_box.margin * 2); + context_box.add(match_widget); + + if (after_message != null && after_message.size > 0) { + context_box.add(get_context_message_widget(after_message.first())); + } + + Label date_label = new Label(ConversationSummary.DefaultSkeletonHeader.get_relative_time(item.display_time)) { xalign=0, visible=true }; + date_label.get_style_context().add_class("dim-label"); + + string display_name = Util.get_conversation_display_name(stream_interactor, item.conversation); + string title = item.message.type_ == Message.Type.GROUPCHAT ? _("In %s").printf(display_name) : _("With %s").printf(display_name); + Box header_box = new Box(Orientation.HORIZONTAL, 10) { margin_left=7, visible=true }; + header_box.add(new Label(@"<b>$(Markup.escape_text(title))</b>") { ellipsize=EllipsizeMode.END, xalign=0, use_markup=true, visible=true }); + header_box.add(date_label); + + Box result_box = new Box(Orientation.VERTICAL, 7) { visible=true }; + result_box.add(header_box); + result_box.add(context_box); + + results_box.add(result_box); + } + } + + private Widget get_match_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); + grid.margin_top = 3; + grid.margin_bottom = 3; + + string text = item.message.body.replace("\n", "").replace("\r", ""); + if (text.length > 200) { + int index = text.index_of(search); + if (index + search.length <= 100) { + text = text.substring(0, 150) + " … " + text.substring(text.length - 50, 50); + } else if (index >= text.length - 100) { + text = text.substring(0, 50) + " … " + text.substring(text.length - 150, 150); + } else { + text = text.substring(0, 25) + " … " + text.substring(index - 50, 50) + text.substring(index, 100) + " … " + text.substring(text.length - 25, 25); + } + } + TextView tv = new TextView() { wrap_mode=Gtk.WrapMode.WORD_CHAR, hexpand=true, visible=true }; + tv.buffer.text = text; + TextTag link_tag = tv.buffer.create_tag("hit", background: "yellow"); + + Gee.List<string> keywords = get_keywords(Regex.escape_string(search.down())); + foreach (string keyword in keywords) { + Regex url_regex = new Regex(keyword.down()); + MatchInfo match_info; + url_regex.match(text.down(), 0, out match_info); + for (; match_info.matches(); match_info.next()) { + int start; + int end; + match_info.fetch_pos(0, out start, out end); + start = text[0:start].char_count(); + end = text[0:end].char_count(); + TextIter start_iter; + TextIter end_iter; + tv.buffer.get_iter_at_offset(out start_iter, start); + tv.buffer.get_iter_at_offset(out end_iter, end); + tv.buffer.apply_tag(link_tag, start_iter, end_iter); + } + } + + grid.attach(tv, 1, 1, 1, 1); + + Button button = new Button() { relief=ReliefStyle.NONE, visible=true }; + button.clicked.connect(() => { + selected_item(item); + }); + button.add(grid); + return button; + } + + private Grid get_context_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); + grid.margin_left = 7; + Label label = new Label(item.message.body.replace("\n", "").replace("\r", "")) { ellipsize=EllipsizeMode.MIDDLE, xalign=0, visible=true }; + grid.attach(label, 1, 1, 1, 1); + grid.opacity = 0.55; + return grid; + } + + private Grid get_skeleton(MessageItem item) { + AvatarImage image = new AvatarImage() { height=32, width=32, margin_right=7, valign=Align.START, visible=true, allow_gray = false }; + image.set_jid(stream_interactor, item.jid, item.message.account); + Grid grid = new Grid() { row_homogeneous=false, visible=true }; + grid.attach(image, 0, 0, 1, 2); + + string display_name = Util.get_display_name(stream_interactor, item.jid, item.message.account); + string color = Util.get_name_hex_color(stream_interactor, item.message.account, item.jid, false); // TODO Util.is_dark_theme(name_label) + Label name_label = new Label("") { use_markup=true, xalign=0, visible=true }; + name_label.label = @"<span size='small' foreground=\"#$color\">$display_name</span>"; + grid.attach(name_label, 1, 0, 1, 1); + return grid; + } + + private static Gee.List<string> get_keywords(string search_string) { + Gee.List<string> ret = new ArrayList<string>(); + foreach (string search in search_string.split(" ")) { + bool is_filter = search.has_prefix("from:") || search.has_prefix("in:") || search.has_prefix("with:"); + if (!is_filter && search != "") { + ret.add(search); + } + } + return ret; + } +} + +} diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index e2798def..61a22085 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -1,11 +1,12 @@ using Gee; +using Gdk; using Gtk; using Dino.Entities; namespace Dino.Ui { -public class UnifiedWindow : Window { +public class UnifiedWindow : Gtk.Window { private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true }; private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true }; @@ -16,7 +17,12 @@ public class UnifiedWindow : Window { private ConversationTitlebar conversation_titlebar; private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true }; private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true }; - private Paned paned = new Paned(Orientation.HORIZONTAL) { visible=true }; + private Paned paned; + private Revealer goto_end_revealer; + private Button goto_end_button; + private Revealer search_revealer; + private SearchEntry search_entry; + private GlobalSearch search_box; private Stack stack = new Stack() { visible=true }; private StreamInteractor stream_interactor; @@ -36,8 +42,47 @@ public class UnifiedWindow : Window { setup_unified(); setup_stack(); - conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_revealer, "reveal-child", - BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + var vadjustment = conversation_frame.scrolled.vadjustment; + vadjustment.notify["value"].connect(() => { + goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size; + }); + goto_end_button.clicked.connect(() => { + conversation_frame.initialize_for_conversation(conversation); + }); + + conversation_titlebar.search_button.clicked.connect(() => { + search_revealer.reveal_child = conversation_titlebar.search_button.active; + }); + search_revealer.notify["child-revealed"].connect(() => { + if (search_revealer.child_revealed) { + if (conversation_frame.conversation != null && search_box.search_entry.text == "") { + reset_search_entry(); + } + search_box.search_entry.grab_focus(); + } + }); + search_box.selected_item.connect((item) => { + on_conversation_selected(item.conversation, false, false); + conversation_frame.initialize_around_message(item.conversation, item); + close_search(); + }); + event.connect((event) => { + if (event.type == EventType.BUTTON_PRESS) { + int dest_x, dest_y; + bool ret = search_box.translate_coordinates(this, 0, 0, out dest_x, out dest_y); + int geometry_x, geometry_y, geometry_width, geometry_height; + this.get_window().get_geometry(out geometry_x, out geometry_y, out geometry_width, out geometry_height); + if (ret && event.button.x_root - geometry_x < dest_x || event.button.y_root - geometry_y < dest_y) { + close_search(); + } + } else if (event.type == EventType.KEY_RELEASE) { + if (event.key.keyval == Gdk.Key.Escape) { + close_search(); + } + } + return false; + }); + paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); focus_in_event.connect(on_focus_in_event); @@ -50,38 +95,60 @@ public class UnifiedWindow : Window { accounts_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("accounts", null); }); conversations_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("add_chat", null); }); conversations_placeholder.secondary_button.clicked.connect(() => { get_application().activate_action("add_conference", null); }); - filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected); - conversation_list_titlebar.conversation_opened.connect(on_conversation_selected); + filterable_conversation_list.conversation_list.conversation_selected.connect((conversation) => on_conversation_selected(conversation)); + conversation_list_titlebar.conversation_opened.connect((conversation) => on_conversation_selected(conversation)); check_stack(); } - public void on_conversation_selected(Conversation conversation) { + private void reset_search_entry() { + if (conversation_frame.conversation != null) { + switch (conversation.type_) { + case Conversation.Type.CHAT: + case Conversation.Type.GROUPCHAT_PM: + search_box.search_entry.text = @"with:$(conversation.counterpart) "; + break; + case Conversation.Type.GROUPCHAT: + search_box.search_entry.text = @"in:$(conversation.counterpart) "; + break; + } + } + } + + public void on_conversation_selected(Conversation conversation, bool do_reset_search = true, bool default_initialize_conversation = true) { if (this.conversation == null || !this.conversation.equals(conversation)) { this.conversation = conversation; stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation); conversation.active = true; // only for conversation_selected filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened + if (do_reset_search) { + reset_search_entry(); + } chat_input.initialize_for_conversation(conversation); - conversation_frame.initialize_for_conversation(conversation); + if (default_initialize_conversation) { + conversation_frame.initialize_for_conversation(conversation); + } conversation_titlebar.initialize_for_conversation(conversation); } } + private void close_search() { + conversation_titlebar.search_button.active = false; + search_revealer.reveal_child = false; + } + private void setup_unified() { - chat_input = new ChatInput.View(stream_interactor) { visible=true }; - conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true }; - filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true }; - - Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true }; - grid.get_style_context().add_class("dino-conversation"); - grid.add(conversation_frame); - grid.add(chat_input); - - paned.set_position(300); - paned.pack1(filterable_conversation_list, false, false); - paned.pack2(grid, true, false); + Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui"); + paned = (Paned) builder.get_object("paned"); + chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor); + conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).init(stream_interactor); + filterable_conversation_list = ((ConversationSelector.View) builder.get_object("conversation_list")).init(stream_interactor); + goto_end_revealer = (Revealer) builder.get_object("goto_end_revealer"); + goto_end_button = (Button) builder.get_object("goto_end_button"); + search_box = ((GlobalSearch) builder.get_object("search_box")).init(stream_interactor); + search_revealer = (Revealer) builder.get_object("search_revealer"); + search_entry = (SearchEntry) builder.get_object("search_entry"); } private void setup_headerbar() { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 3cadfffb..4e9e942d 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -118,10 +118,6 @@ public static void force_background(Gtk.Widget widget, string color, string sele force_css(widget, force_background_css.printf(selector, color)); } -public static void force_base_background(Gtk.Widget widget, string selector = "*") { - force_background(widget, "@theme_base_color", selector); -} - public static void force_color(Gtk.Widget widget, string color, string selector = "*") { force_css(widget, force_color_css.printf(selector, color)); } @@ -142,4 +138,13 @@ public static bool is_24h_format() { return settings_format == "24h" || p_format == " "; } +// Workaround GTK TextView issues +public static 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); +} + } |