initial search logic / display
This commit is contained in:
parent
8b23ddad2d
commit
61915ca566
|
@ -42,6 +42,7 @@ SOURCES
|
||||||
src/service/notification_events.vala
|
src/service/notification_events.vala
|
||||||
src/service/presence_manager.vala
|
src/service/presence_manager.vala
|
||||||
src/service/roster_manager.vala
|
src/service/roster_manager.vala
|
||||||
|
src/service/search_processor.vala
|
||||||
src/service/stream_interactor.vala
|
src/service/stream_interactor.vala
|
||||||
src/service/util.vala
|
src/service/util.vala
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ public interface Dino.Application : GLib.Application {
|
||||||
FileManager.start(stream_interactor, db);
|
FileManager.start(stream_interactor, db);
|
||||||
NotificationEvents.start(stream_interactor);
|
NotificationEvents.start(stream_interactor);
|
||||||
ContentItemAccumulator.start(stream_interactor);
|
ContentItemAccumulator.start(stream_interactor);
|
||||||
|
SearchProcessor.start(stream_interactor, db);
|
||||||
|
|
||||||
create_actions();
|
create_actions();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Gee;
|
using Gee;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
using Dino.Entities;
|
using Dino.Entities;
|
||||||
|
|
||||||
|
|
54
libdino/src/service/search_processor.vala
Normal file
54
libdino/src/service/search_processor.vala
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Qlite;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino {
|
||||||
|
|
||||||
|
public class SearchProcessor : StreamInteractionModule, Object {
|
||||||
|
public static ModuleIdentity<SearchProcessor> IDENTITY = new ModuleIdentity<SearchProcessor>("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<Message> match_messages(string match, int offset = -1) {
|
||||||
|
Gee.List<Message> ret = new ArrayList<Message>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ set(RESOURCE_LIST
|
||||||
chat_input.ui
|
chat_input.ui
|
||||||
contact_details_dialog.ui
|
contact_details_dialog.ui
|
||||||
conversation_list_titlebar.ui
|
conversation_list_titlebar.ui
|
||||||
|
global_search.ui
|
||||||
conversation_selector/view.ui
|
conversation_selector/view.ui
|
||||||
conversation_selector/chat_row_tooltip.ui
|
conversation_selector/chat_row_tooltip.ui
|
||||||
conversation_selector/conversation_row.ui
|
conversation_selector/conversation_row.ui
|
||||||
|
@ -94,6 +95,7 @@ SOURCES
|
||||||
src/ui/contact_details/dialog.vala
|
src/ui/contact_details/dialog.vala
|
||||||
src/ui/contact_details/muc_config_form_provider.vala
|
src/ui/contact_details/muc_config_form_provider.vala
|
||||||
src/ui/conversation_list_titlebar.vala
|
src/ui/conversation_list_titlebar.vala
|
||||||
|
src/ui/global_search.vala
|
||||||
src/ui/conversation_selector/chat_row.vala
|
src/ui/conversation_selector/chat_row.vala
|
||||||
src/ui/conversation_selector/conversation_row.vala
|
src/ui/conversation_selector/conversation_row.vala
|
||||||
src/ui/conversation_selector/groupchat_pm_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_summary/subscription_notification.vala
|
||||||
src/ui/conversation_titlebar/menu_entry.vala
|
src/ui/conversation_titlebar/menu_entry.vala
|
||||||
src/ui/conversation_titlebar/occupants_entry.vala
|
src/ui/conversation_titlebar/occupants_entry.vala
|
||||||
|
src/ui/conversation_titlebar/search_entry.vala
|
||||||
src/ui/conversation_titlebar/view.vala
|
src/ui/conversation_titlebar/view.vala
|
||||||
src/ui/manage_accounts/account_row.vala
|
src/ui/manage_accounts/account_row.vala
|
||||||
src/ui/manage_accounts/add_account_dialog.vala
|
src/ui/manage_accounts/add_account_dialog.vala
|
||||||
|
|
35
main/data/global_search.ui
Normal file
35
main/data/global_search.ui
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="DinoUiGlobalSearch" parent="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="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="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>
|
||||||
|
</template>
|
||||||
|
</interface>
|
|
@ -26,6 +26,11 @@ window.dino-main .dino-sidebar frame.collapsed {
|
||||||
border-bottom: 1px solid @borders;
|
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 {
|
window.dino-main .dino-chatinput frame box {
|
||||||
background: @theme_base_color;
|
background: @theme_base_color;
|
||||||
|
|
|
@ -49,16 +49,8 @@
|
||||||
<property name="width-request">400</property>
|
<property name="width-request">400</property>
|
||||||
<property name="shadow-type">none</property>
|
<property name="shadow-type">none</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="DinoUiGlobalSearch" id="search_box">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="visible">True</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><placeholder/></child>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -72,4 +64,4 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box {
|
||||||
return datetime.format(format);
|
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();
|
DateTime now = new DateTime.now_local();
|
||||||
TimeSpan timespan = now.difference(datetime);
|
TimeSpan timespan = now.difference(datetime);
|
||||||
if (timespan > 365 * TimeSpan.DAY) {
|
if (timespan > 365 * TimeSpan.DAY) {
|
||||||
|
|
174
main/src/ui/global_search.vala
Normal file
174
main/src/ui/global_search.vala
Normal file
|
@ -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<Message> 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>" + _("%i search results").printf(match_count) + "</i>";
|
||||||
|
Gee.List<Message> messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search);
|
||||||
|
loaded_results += messages.size;
|
||||||
|
append_messages(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append_messages(Gee.List<Message> 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<Message> before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, message.local_time, message.id, 1);
|
||||||
|
Gee.List<Message> 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(@"<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = @"<span size='small' foreground=\"#$color\">$display_name</span>";
|
||||||
|
grid.attach(name_label, 1, 0, 1, 1);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ public class UnifiedWindow : Window {
|
||||||
private Paned paned;
|
private Paned paned;
|
||||||
private Revealer search_revealer;
|
private Revealer search_revealer;
|
||||||
private SearchEntry search_entry;
|
private SearchEntry search_entry;
|
||||||
|
private GlobalSearch search_box;
|
||||||
private Stack stack = new Stack() { visible=true };
|
private Stack stack = new Stack() { visible=true };
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
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);
|
conversation_titlebar.search_button.bind_property("active", search_revealer, "reveal-child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
|
||||||
search_revealer.notify["child-revealed"].connect(() => {
|
search_revealer.notify["child-revealed"].connect(() => {
|
||||||
if (search_revealer.child_revealed) {
|
if (search_revealer.child_revealed) {
|
||||||
search_entry.grab_focus();
|
search_box.search_entry.grab_focus();
|
||||||
} else {
|
} 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);
|
chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor);
|
||||||
conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).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);
|
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_revealer = (Revealer) builder.get_object("search_revealer");
|
||||||
search_entry = (SearchEntry) builder.get_object("search_entry");
|
search_entry = (SearchEntry) builder.get_object("search_entry");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue