From 3ea00446fb5893804243f5b1a1aa89817b7bc19a Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 19 Jun 2018 18:07:00 +0200 Subject: refactor conversation item management (accumulate them in libdino) --- main/CMakeLists.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'main/CMakeLists.txt') diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9c5b06ff..d5f16992 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -100,16 +100,12 @@ 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 -- cgit v1.2.3-54-g00ecf From 8b23ddad2d33a1504cd28c0df583dfe50cadccda Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 4 Jul 2018 23:38:28 +0200 Subject: ui: search sidebar initial --- main/CMakeLists.txt | 1 + main/data/conversation_list_titlebar.ui | 15 ----- main/data/conversation_selector/view.ui | 15 ----- main/data/menu_add.ui | 1 + main/data/menu_app.ui | 1 + main/data/menu_conversation.ui | 1 + main/data/theme.css | 10 +++ main/data/unified_main_content.ui | 75 ++++++++++++++++++++++ main/src/ui/chat_input/view.vala | 3 +- main/src/ui/conversation_list_titlebar.vala | 1 - main/src/ui/conversation_selector/view.vala | 33 +--------- .../ui/conversation_summary/conversation_view.vala | 3 +- .../src/ui/conversation_titlebar/search_entry.vala | 32 +++++++++ main/src/ui/conversation_titlebar/view.vala | 3 + main/src/ui/unified_window.vala | 46 ++++++++----- 15 files changed, 161 insertions(+), 79 deletions(-) create mode 100644 main/data/unified_main_content.ui create mode 100644 main/src/ui/conversation_titlebar/search_entry.vala (limited to 'main/CMakeLists.txt') diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d5f16992..d71dd0ef 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -44,6 +44,7 @@ set(RESOURCE_LIST occupant_list.ui occupant_list_item.ui settings_dialog.ui + unified_main_content.ui unified_window_placeholder.ui theme.css 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 @@ start - - - True - - - True - system-search-symbolic - 1 - - - - - end - - 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 @@ -4,21 +4,6 @@ True True vertical - - - True - True - - - edit-find-symbolic - Search - 10px - True - True - - - - True 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 @@ +
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 @@ +
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 @@ +
diff --git a/main/data/theme.css b/main/data/theme.css index e7d58ffb..52ca1af7 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -17,6 +17,16 @@ window.dino-main .dino-conversation undershoot { background: none; } +window.dino-main .dino-sidebar frame { + background: @insensitive_bg_color; + border-left: 1px solid @borders; +} + +window.dino-main .dino-sidebar frame.collapsed { + border-bottom: 1px solid @borders; +} + + window.dino-main .dino-chatinput frame box { background: @theme_base_color; } diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui new file mode 100644 index 00000000..9b396b34 --- /dev/null +++ b/main/data/unified_main_content.ui @@ -0,0 +1,75 @@ + + + + 300 + horizontal + True + + + True + + + False + False + + + + + True + + + vertical + True + + + + True + + + + + True + + + + + + + True + end + slide-left + + + + True + 400 + none + + + vertical + True + + + True + 12 + + + + + + + + + + + + True + False + + + + \ No newline at end of file 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/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 008909e4..870b6ee3 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -35,7 +35,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private bool firstLoad = true; private bool at_current_content = 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); @@ -58,6 +58,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { }); Util.force_base_background(this); + return this; } // Workaround GTK TextView issues: Delay first load of contents 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..e80e5954 --- /dev/null +++ b/main/src/ui/conversation_titlebar/search_entry.vala @@ -0,0 +1,32 @@ +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) { + active = false; + } +} + +} 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 widgets = new ArrayList(); + 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/unified_window.vala b/main/src/ui/unified_window.vala index e2798def..3292aa3d 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -16,7 +16,9 @@ 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 search_revealer; + private SearchEntry search_entry; private Stack stack = new Stack() { visible=true }; private StreamInteractor stream_interactor; @@ -36,8 +38,15 @@ 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); + conversation_titlebar.search_button.bind_property("active", search_revealer, "reveal-child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + search_revealer.notify["child-revealed"].connect(() => { + if (search_revealer.child_revealed) { + search_entry.grab_focus(); + } else { + search_entry.text = ""; + } + }); + paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); focus_in_event.connect(on_focus_in_event); @@ -56,6 +65,18 @@ public class UnifiedWindow : Window { check_stack(); } + private void hide_search_results() { + search_revealer.get_style_context().add_class("collapsed"); + search_revealer.valign = Align.START; + // TODO: Make search results box inivisble + } + + private void show_search_results() { + // TODO: Make search results box visible + search_revealer.get_style_context().remove_class("collapsed"); + search_revealer.valign = Align.FILL; + } + public void on_conversation_selected(Conversation conversation) { if (this.conversation == null || !this.conversation.equals(conversation)) { this.conversation = conversation; @@ -70,18 +91,13 @@ public class UnifiedWindow : Window { } 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); + search_revealer = (Revealer) builder.get_object("search_revealer"); + search_entry = (SearchEntry) builder.get_object("search_entry"); } private void setup_headerbar() { -- cgit v1.2.3-54-g00ecf From 61915ca56617e8f45ae8bd85cb87f0b8a9a895b0 Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 10 Jul 2018 00:31:39 +0200 Subject: initial search logic / display --- libdino/CMakeLists.txt | 1 + libdino/src/application.vala | 1 + libdino/src/service/message_storage.vala | 1 + libdino/src/service/search_processor.vala | 54 +++++++ main/CMakeLists.txt | 3 + main/data/global_search.ui | 35 +++++ main/data/theme.css | 5 + main/data/unified_main_content.ui | 12 +- .../conversation_item_skeleton.vala | 2 +- main/src/ui/global_search.vala | 174 +++++++++++++++++++++ main/src/ui/unified_window.vala | 6 +- 11 files changed, 281 insertions(+), 13 deletions(-) create mode 100644 libdino/src/service/search_processor.vala create mode 100644 main/data/global_search.ui create mode 100644 main/src/ui/global_search.vala (limited to 'main/CMakeLists.txt') diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 429fc1f3..de44195d 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -42,6 +42,7 @@ SOURCES src/service/notification_events.vala src/service/presence_manager.vala src/service/roster_manager.vala + src/service/search_processor.vala src/service/stream_interactor.vala src/service/util.vala diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 0edd6df6..80e474ac 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -39,6 +39,7 @@ public interface Dino.Application : GLib.Application { FileManager.start(stream_interactor, db); NotificationEvents.start(stream_interactor); ContentItemAccumulator.start(stream_interactor); + SearchProcessor.start(stream_interactor, db); create_actions(); diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index e3869e41..abc8acb4 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -1,4 +1,5 @@ using Gee; +using Qlite; using Dino.Entities; diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala new file mode 100644 index 00000000..3c1057ae --- /dev/null +++ b/libdino/src/service/search_processor.vala @@ -0,0 +1,54 @@ +using Gee; + +using Xmpp; +using Qlite; +using Dino.Entities; + +namespace Dino { + +public class SearchProcessor : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("search_processor"); + public string id { get { return IDENTITY.id; } } + + private StreamInteractor stream_interactor; + private Database db; + + public static void start(StreamInteractor stream_interactor, Database db) { + SearchProcessor m = new SearchProcessor(stream_interactor, db); + stream_interactor.add_module(m); + } + + public SearchProcessor(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + } + + public Gee.List match_messages(string match, int offset = -1) { + Gee.List ret = new ArrayList(Message.equals_func); + var query = db.message + .match(db.message.body, parse_search(match)) + .order_by(db.message.id, "DESC") + .limit(10); + if (offset > 0) { + query.offset(offset); + } + foreach (Row row in query) { + ret.add(new Message.from_row(db, row)); + } + return ret; + } + + public int count_match_messages(string match) { + return (int)db.message.match(db.message.body, parse_search(match)).count(); + } + + private string parse_search(string search) { + string ret = ""; + foreach(string word in search.split(" ")) { + ret += word + "* "; + } + return ret; + } +} + +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d71dd0ef..1af08217 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 @@ -94,6 +95,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 @@ -110,6 +112,7 @@ SOURCES 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/global_search.ui b/main/data/global_search.ui new file mode 100644 index 00000000..cc5f043b --- /dev/null +++ b/main/data/global_search.ui @@ -0,0 +1,35 @@ + + + + diff --git a/main/data/theme.css b/main/data/theme.css index 52ca1af7..61f15af4 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -26,6 +26,11 @@ window.dino-main .dino-sidebar frame.collapsed { border-bottom: 1px solid @borders; } +window.dino-main .dino-sidebar textview, +window.dino-main .dino-sidebar textview text { + background-color: transparent; +} + window.dino-main .dino-chatinput frame box { background: @theme_base_color; diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui index 9b396b34..61781ac4 100644 --- a/main/data/unified_main_content.ui +++ b/main/data/unified_main_content.ui @@ -49,16 +49,8 @@ 400 none - - vertical + True - - - True - 12 - - - @@ -72,4 +64,4 @@
- \ No newline at end of file + 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/global_search.vala b/main/src/ui/global_search.vala new file mode 100644 index 00000000..cadee9c1 --- /dev/null +++ b/main/src/ui/global_search.vala @@ -0,0 +1,174 @@ +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")] +class GlobalSearch : Box { + 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; + + public GlobalSearch init(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + search_entry.search_changed.connect(() => { + set_search(search_entry.text); + }); + + 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 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(); + }); + return this; + } + + private void clear_search() { + results_box.@foreach((widget) => { widget.destroy(); }); + } + + private void set_search(string search) { + clear_search(); + this.search = search; + + int match_count = stream_interactor.get_module(SearchProcessor.IDENTITY).count_match_messages(search); + entry_number_label.label = "" + _("%i search results").printf(match_count) + ""; + Gee.List messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search); + loaded_results += messages.size; + append_messages(messages); + } + + private void append_messages(Gee.List messages) { + foreach (Message message in messages) { + if (message.from == null) { + print("wtf null\n"); + continue; + } + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message); + Gee.List before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, message.local_time, message.id, 1); + Gee.List after_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(conversation, message.local_time, 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())); + } + context_box.add(get_match_message_widget(message)); + 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(message.time)) { xalign=0, visible=true }; + date_label.get_style_context().add_class("dim-label"); + + string display_name = Util.get_conversation_display_name(stream_interactor, conversation); + string title = 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(@"$(Markup.escape_text(title))") { 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); + } + } + + // 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 Widget get_match_message_widget(Message message) { + Grid grid = get_skeleton(message); + grid.margin_top = 3; + grid.margin_bottom = 3; + + string text = 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"); + + Regex url_regex = new Regex(search.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); + + // force_alloc_width(tv, this.width_request); + + Button button = new Button() { relief=ReliefStyle.NONE, visible=true }; + button.add(grid); + return button; + } + + private Grid get_context_message_widget(Message message) { + Grid grid = get_skeleton(message); + grid.margin_left = 7; + Label label = new Label(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(Message message) { + AvatarImage image = new AvatarImage() { height=32, width=32, margin_right=7, valign=Align.START, visible=true, allow_gray = false }; + image.set_jid(stream_interactor, message.from, 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, message.from, message.account); + string color = Util.get_name_hex_color(stream_interactor, message.account, message.from, false); // TODO Util.is_dark_theme(name_label) + Label name_label = new Label("") { use_markup=true, xalign=0, visible=true }; + name_label.label = @"$display_name"; + grid.attach(name_label, 1, 0, 1, 1); + return grid; + } +} + +} diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 3292aa3d..e5444f9d 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -19,6 +19,7 @@ public class UnifiedWindow : Window { private Paned paned; private Revealer search_revealer; private SearchEntry search_entry; + private GlobalSearch search_box; private Stack stack = new Stack() { visible=true }; private StreamInteractor stream_interactor; @@ -41,9 +42,9 @@ public class UnifiedWindow : Window { conversation_titlebar.search_button.bind_property("active", search_revealer, "reveal-child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); search_revealer.notify["child-revealed"].connect(() => { if (search_revealer.child_revealed) { - search_entry.grab_focus(); + search_box.search_entry.grab_focus(); } else { - search_entry.text = ""; + search_box.search_entry.text = ""; } }); @@ -96,6 +97,7 @@ public class UnifiedWindow : Window { 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); + 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"); } -- cgit v1.2.3-54-g00ecf From c0844bdea428c10949339960bd16ea5e2a335fb8 Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 1 Aug 2018 15:20:56 +0200 Subject: add suggestions/auto-complete for search filters --- libdino/src/service/search_processor.vala | 159 ++++++++++++++++++- main/CMakeLists.txt | 1 + main/data/global_search.ui | 249 ++++++++++++++++-------------- main/data/search_autocomplete.ui | 24 +++ main/data/theme.css | 19 ++- main/data/unified_main_content.ui | 4 +- main/src/ui/global_search.vala | 68 +++++++- qlite/src/query_builder.vala | 16 +- 8 files changed, 410 insertions(+), 130 deletions(-) create mode 100644 main/data/search_autocomplete.ui (limited to 'main/CMakeLists.txt') diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala index e56efa41..3f746981 100644 --- a/libdino/src/service/search_processor.vala +++ b/libdino/src/service/search_processor.vala @@ -31,19 +31,19 @@ public class SearchProcessor : StreamInteractionModule, Object { foreach(string word in query.split(" ")) { if (word.has_prefix("with:")) { if (with == null) { - with = word.substring(5) + "%"; + with = word.substring(5); } else { return db.message.select().where("0"); } } else if (word.has_prefix("in:")) { if (in_ == null) { - in_ = word.substring(3) + "%"; + in_ = word.substring(3); } else { return db.message.select().where("0"); } } else if (word.has_prefix("from:")) { if (from == null) { - from = word.substring(5) + "%"; + from = word.substring(5); } else { return db.message.select().where("0"); } @@ -90,9 +90,143 @@ public class SearchProcessor : StreamInteractionModule, Object { return rows; } + public Gee.List suggest_auto_complete(string query, int cursor_position, int limit = 5) { + int after_prev_space = query.substring(0, cursor_position).last_index_of(" ") + 1; + int next_space = query.index_of(" ", after_prev_space); + if (next_space < 0) next_space = query.length; + string current_query = query.substring(after_prev_space, next_space - after_prev_space); + Gee.List suggestions = new ArrayList(); + + if (current_query.has_prefix("from:")) { + if (cursor_position < after_prev_space + 5) return suggestions; + string current_from = current_query.substring(5); + string[] splitted = query.split(" "); + foreach(string s in splitted) { + if (s.has_prefix("from:") && s != "from:" + current_from) { + // Already have an from: filter -> no useful autocompletion possible + return suggestions; + } + } + string? current_in = null; + string? current_with = null; + foreach(string s in splitted) { + if (s.has_prefix("in:")) { + current_in = s.substring(3); + } else if (s.has_prefix("with:")) { + current_with = s.substring(5); + } + } + if (current_in != null && current_with != null) { + // in: and with: -> no useful autocompletion possible + return suggestions; + } + if (current_with != null) { + // Can only be the other one or us + + // Normal chat + QueryBuilder chats = db.conversation.select() + .join_with(db.jid, db.jid.id, db.conversation.jid_id) + .join_with(db.account, db.account.id, db.conversation.account_id) + .with(db.jid.bare_jid, "=", current_with) + .with(db.account.enabled, "=", true) + .with(db.conversation.type_, "=", Conversation.Type.CHAT) + .order_by(db.conversation.last_active, "DESC"); + foreach(Row chat in chats) { + if (suggestions.size == 0) { + suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "from:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + } + suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.account.bare_jid]), "from:"+chat[db.account.bare_jid], after_prev_space, next_space)); + } + return suggestions; + } + if (current_in != null) { + // All members of the MUC with history + QueryBuilder msgs = db.message.select() + .select_string(@"account.*, $(db.message.counterpart_resource)") + .join_with(db.jid, db.jid.id, db.message.counterpart_id) + .join_with(db.account, db.account.id, db.message.account_id) + .with(db.jid.bare_jid, "=", current_in) + .with(db.account.enabled, "=", true) + .with(db.message.type_, "=", Message.Type.GROUPCHAT) + .with(db.message.counterpart_resource, "LIKE", @"%$current_from%") + .group_by({db.message.counterpart_resource}) + .order_by_name(@"MAX($(db.message.time))", "DESC") + .limit(5); + foreach(Row msg in msgs) { + suggestions.add(new SearchSuggestion(new Account.from_row(db, msg), new Jid(current_in).with_resource(msg[db.message.counterpart_resource]), "from:"+msg[db.message.counterpart_resource], after_prev_space, next_space)); + } + } + // TODO: auto complete from + } else if (current_query.has_prefix("with:")) { + if (cursor_position < after_prev_space + 5) return suggestions; + string current_with = current_query.substring(5); + string[] splitted = query.split(" "); + foreach(string s in splitted) { + if ((s.has_prefix("with:") && s != "with:" + current_with) || s.has_prefix("in:")) { + // Already have an in: or with: filter -> no useful autocompletion possible + return suggestions; + } + } + + // Normal chat + QueryBuilder chats = db.conversation.select() + .join_with(db.jid, db.jid.id, db.conversation.jid_id) + .join_with(db.account, db.account.id, db.conversation.account_id) + .outer_join_on(db.roster, @"$(db.jid.bare_jid) = $(db.roster.jid) AND $(db.account.id) = $(db.roster.account_id)") + .where(@"$(db.jid.bare_jid) LIKE ? OR $(db.roster.handle) LIKE ?", {@"%$current_with%", @"%$current_with%"}) + .with(db.account.enabled, "=", true) + .with(db.conversation.type_, "=", Conversation.Type.CHAT) + .order_by(db.conversation.last_active, "DESC") + .limit(limit); + foreach(Row chat in chats) { + suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "with:"+chat[db.jid.bare_jid], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + } + + // Groupchat PM + if (suggestions.size < 5) { + chats = db.conversation.select() + .join_with(db.jid, db.jid.id, db.conversation.jid_id) + .join_with(db.account, db.account.id, db.conversation.account_id) + .where(@"$(db.jid.bare_jid) LIKE ? OR $(db.conversation.resource) LIKE ?", {@"%$current_with%", @"%$current_with%"}) + .with(db.account.enabled, "=", true) + .with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT_PM) + .order_by(db.conversation.last_active, "DESC") + .limit(limit - suggestions.size); + foreach(Row chat in chats) { + suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]).with_resource(chat[db.conversation.resource]), "with:"+chat[db.jid.bare_jid]+"/"+chat[db.conversation.resource], after_prev_space, next_space) { order = chat[db.conversation.last_active]}); + } + suggestions.sort((a, b) => (int)(b.order - a.order)); + } + } else if (current_query.has_prefix("in:")) { + if (cursor_position < after_prev_space + 3) return suggestions; + string current_in = current_query.substring(3); + string[] splitted = query.split(" "); + foreach(string s in splitted) { + if ((s.has_prefix("in:") && s != "in:" + current_in) || s.has_prefix("with:")) { + // Already have an in: or with: filter -> no useful autocompletion possible + return suggestions; + } + } + QueryBuilder groupchats = db.conversation.select() + .join_with(db.jid, db.jid.id, db.conversation.jid_id) + .join_with(db.account, db.account.id, db.conversation.account_id) + .with(db.jid.bare_jid, "LIKE", @"%$current_in%") + .with(db.account.enabled, "=", true) + .with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT) + .order_by(db.conversation.last_active, "DESC") + .limit(limit); + foreach(Row chat in groupchats) { + suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "in:"+chat[db.jid.bare_jid], after_prev_space, next_space)); + } + } else { + // Other auto complete? + } + return suggestions; + } + public Gee.List match_messages(string query, int offset = -1) { Gee.List ret = new ArrayList(); - var rows = prepare_search(query, true).limit(10); + QueryBuilder rows = prepare_search(query, false).limit(10); if (offset > 0) { rows.offset(offset); } @@ -109,4 +243,21 @@ public class SearchProcessor : StreamInteractionModule, Object { } } +public class SearchSuggestion : Object { + public Account account { get; private set; } + public Jid? jid { get; private set; } + public string completion { get; private set; } + public int start_index { get; private set; } + public int end_index { get; private set; } + public long order { get; set; } + + public SearchSuggestion(Account account, Jid? jid, string completion, int start_index, int end_index) { + this.account = account; + this.jid = jid; + this.completion = completion; + this.start_index = start_index; + this.end_index = end_index; + } +} + } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 1af08217..49b1a9fc 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -44,6 +44,7 @@ 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 diff --git a/main/data/global_search.ui b/main/data/global_search.ui index 3c4597c1..44abf6de 100644 --- a/main/data/global_search.ui +++ b/main/data/global_search.ui @@ -1,144 +1,167 @@ -