From c4928d46486c4117b1bf2738d114297a42cf1940 Mon Sep 17 00:00:00 2001 From: bobufa Date: Mon, 18 Jun 2018 01:47:43 +0200 Subject: [PATCH 01/21] add support for fts tables to qlite --- qlite/src/database.vala | 5 ++++ qlite/src/query_builder.vala | 45 +++++++++++++++++++++++++-------- qlite/src/table.vala | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/qlite/src/database.vala b/qlite/src/database.vala index 37a7b7f7..d13b9bc4 100644 --- a/qlite/src/database.vala +++ b/qlite/src/database.vala @@ -90,6 +90,11 @@ public class Database { return new QueryBuilder(this).select(columns); } + internal MatchQueryBuilder match_query(Table table) { + ensure_init(); + return new MatchQueryBuilder(this, table); + } + public InsertBuilder insert() { ensure_init(); return new InsertBuilder(this); diff --git a/qlite/src/query_builder.vala b/qlite/src/query_builder.vala index 915e2d2d..9ca5bf25 100644 --- a/qlite/src/query_builder.vala +++ b/qlite/src/query_builder.vala @@ -10,12 +10,15 @@ public class QueryBuilder : StatementBuilder { private Column[] columns = {}; // FROM [...] - private Table? table; - private string? table_name; + protected Table? table; + protected string? table_name; + + // JOIN [...] + private string joins = ""; // WHERE [...] - private string selection = "1"; - private StatementBuilder.AbstractField[] selection_args = {}; + protected string selection = "1"; + internal StatementBuilder.AbstractField[] selection_args = {}; // ORDER BY [...] private OrderingTerm[]? order_by_terms = {}; @@ -50,18 +53,23 @@ public class QueryBuilder : StatementBuilder { return this; } - public QueryBuilder from(Table table) { + public virtual QueryBuilder from(Table table) { if (this.table_name != null) error("cannot use from() multiple times."); this.table = table; this.table_name = table.name; return this; } - public QueryBuilder from_name(string table) { + public virtual QueryBuilder from_name(string table) { this.table_name = table; return this; } + public QueryBuilder join(string table, string on) { + joins += @"JOIN $table ON $on"; + return this; + } + public QueryBuilder where(string selection, string[] selection_args = {}) { if (this.selection != "1") error("selection was already done, but where() was called."); this.selection = selection; @@ -74,17 +82,17 @@ public class QueryBuilder : StatementBuilder { public QueryBuilder with(Column column, string comp, T value) { if ((column.unique || column.primary_key) && comp == "=") single_result = true; selection_args += new Field(column, value); - selection = @"($selection) AND $(column.name) $comp ?"; + selection = @"($selection) AND $table_name.$(column.name) $comp ?"; return this; } public QueryBuilder with_null(Column column) { - selection = @"($selection) AND $(column.name) ISNULL"; + selection = @"($selection) AND $table_name.$(column.name) ISNULL"; return this; } public QueryBuilder without_null(Column column) { - selection = @"($selection) AND $(column.name) NOT NULL"; + selection = @"($selection) AND $table_name.$(column.name) NOT NULL"; return this; } @@ -135,7 +143,7 @@ public class QueryBuilder : StatementBuilder { } internal override Statement prepare() { - Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") WHERE $selection $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")"); + Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") $joins WHERE $selection $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")"); for (int i = 0; i < selection_args.length; i++) { selection_args[i].bind(stmt, i+1); } @@ -177,4 +185,21 @@ public class QueryBuilder : StatementBuilder { } } +public class MatchQueryBuilder : QueryBuilder { + internal MatchQueryBuilder(Database db, Table table) { + base(db); + if (table.fts_columns == null) error("MATCH query on non FTS table"); + from(table); + join(@"_fts_$table_name", @"_fts_$table_name.docid = $table_name.rowid"); + } + + public MatchQueryBuilder match(Column column, string match) { + if (table == null) error("MATCH must occur after FROM statement"); + if (!(column in table.fts_columns)) error("MATCH selection on non FTS column"); + selection_args += new StatementBuilder.StringField(match); + selection = @"($selection) AND _fts_$table_name.$(column.name) MATCH ?"; + return this; + } +} + } diff --git a/qlite/src/table.vala b/qlite/src/table.vala index 00b4ef00..8725c4c9 100644 --- a/qlite/src/table.vala +++ b/qlite/src/table.vala @@ -8,6 +8,8 @@ public class Table { protected Column[]? columns; private string constraints = ""; private string[] post_statements = {}; + private string[] create_statements = {}; + internal Column[]? fts_columns; public Table(Database db, string name) { this.db = db; @@ -19,6 +21,33 @@ public class Table { this.constraints = constraints; } + public void fts(Column[] columns) { + if (fts_columns != null) error("Only one FTS index may be used per table."); + fts_columns = columns; + string cs = ""; + string cnames = ""; + string cnews = ""; + foreach (Column c in columns) { + cs += @", $c"; + cnames += @", $(c.name)"; + cnews += @", new.$(c.name)"; + } + add_create_statement(@"CREATE VIRTUAL TABLE IF NOT EXISTS _fts_$name USING fts4(tokenize=unicode61, content=\"$name\"$cs)"); + add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_bu_$(name) BEFORE UPDATE ON $name BEGIN DELETE FROM _fts_$name WHERE docid=old.rowid; END"); + add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_bd_$(name) BEFORE DELETE ON $name BEGIN DELETE FROM _fts_$name WHERE docid=old.rowid; END"); + add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_au_$(name) AFTER UPDATE ON $name BEGIN INSERT INTO _fts_$name(docid$cnames) VALUES(new.rowid$cnews); END"); + add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_ai_$(name) AFTER INSERT ON $name BEGIN INSERT INTO _fts_$name(docid$cnames) VALUES(new.rowid$cnews); END"); + } + + public void fts_rebuild() { + if (fts_columns == null) error("FTS not available on this table."); + try { + db.exec(@"INSERT INTO _fts_$name(_fts_$name) VALUES('rebuild');"); + } catch (Error e) { + error("Qlite Error: Rebuilding FTS index"); + } + } + public void unique(Column[] columns, string? on_conflict = null) { constraints += ", UNIQUE ("; bool first = true; @@ -37,6 +66,10 @@ public class Table { post_statements += stmt; } + public void add_create_statement(string stmt) { + create_statements += stmt; + } + public void index(string index_name, Column[] columns, bool unique = false) { string stmt = @"CREATE $(unique ? "UNIQUE" : "") INDEX IF NOT EXISTS $index_name ON $name ("; bool first = true; @@ -58,6 +91,15 @@ public class Table { return db.select(columns).from(this); } + private MatchQueryBuilder match_query() { + ensure_init(); + return db.match_query(this); + } + + public MatchQueryBuilder match(Column column, string query) { + return match_query().match(column, query); + } + public InsertBuilder insert() { ensure_init(); return db.insert().into(this); @@ -107,6 +149,13 @@ public class Table { } catch (Error e) { error("Qlite Error: Create table at version"); } + foreach (string stmt in create_statements) { + try { + db.exec(stmt); + } catch (Error e) { + error("Qlite Error: Create table at version"); + } + } } public void add_columns_for_version(long old_version, long new_version) { From ab0bc7f04d156db9e398c3f8c4f8bdfc11dd7458 Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 20 Jun 2018 16:22:03 +0200 Subject: [PATCH 02/21] enable fts for the body of messages --- libdino/src/service/database.vala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 25db82f8..2dca686f 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 6; + private const int VERSION = 7; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -54,6 +54,7 @@ public class Database : Qlite.Database { init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction, type_, time, local_time, body, encryption, marked}); index("message_localtime_counterpart_idx", {local_time, counterpart_id}); + fts({body}); } } @@ -206,6 +207,9 @@ public class Database : Qlite.Database { public override void migrate(long oldVersion) { // new table columns are added, outdated columns are still present + if (oldVersion < 7) { + message.fts_rebuild(); + } } public ArrayList get_accounts() { From 3ea00446fb5893804243f5b1a1aa89817b7bc19a Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 19 Jun 2018 18:07:00 +0200 Subject: [PATCH 03/21] refactor conversation item management (accumulate them in libdino) --- libdino/CMakeLists.txt | 1 + libdino/src/application.vala | 1 + libdino/src/entity/file_transfer.vala | 20 +- libdino/src/plugin/interfaces.vala | 26 +- libdino/src/plugin/registry.vala | 21 +- .../src/service/content_item_accumulator.vala | 224 ++++++++++++++++++ libdino/src/service/file_manager.vala | 37 ++- libdino/src/service/message_storage.vala | 32 +-- main/CMakeLists.txt | 8 +- .../chat_state_populator.vala | 4 +- .../content_item_widget_factory.vala | 224 ++++++++++++++++++ .../content_populator.vala | 99 ++++++++ .../conversation_view.vala | 35 +-- .../date_separator_populator.vala | 4 +- .../default_file_display.vala | 95 -------- .../default_message_display.vala | 58 ----- .../conversation_summary/file_populator.vala | 54 ----- .../conversation_summary/image_display.vala | 137 ----------- .../message_populator.vala | 81 ------- .../message_textview.vala | 4 +- .../slashme_message_display.vala | 79 ------ plugins/http-files/src/manager.vala | 19 +- plugins/http-files/src/plugin.vala | 2 +- 23 files changed, 660 insertions(+), 605 deletions(-) create mode 100644 libdino/src/service/content_item_accumulator.vala create mode 100644 main/src/ui/conversation_summary/content_item_widget_factory.vala create mode 100644 main/src/ui/conversation_summary/content_populator.vala delete mode 100644 main/src/ui/conversation_summary/default_file_display.vala delete mode 100644 main/src/ui/conversation_summary/default_message_display.vala delete mode 100644 main/src/ui/conversation_summary/file_populator.vala delete mode 100644 main/src/ui/conversation_summary/image_display.vala delete mode 100644 main/src/ui/conversation_summary/message_populator.vala delete mode 100644 main/src/ui/conversation_summary/slashme_message_display.vala diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 62c73eca..429fc1f3 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -29,6 +29,7 @@ SOURCES src/service/blocking_manager.vala src/service/chat_interaction.vala src/service/connection_manager.vala + src/service/content_item_accumulator.vala src/service/conversation_manager.vala src/service/counterpart_interaction_manager.vala src/service/database.vala diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 370618b2..0edd6df6 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -38,6 +38,7 @@ public interface Dino.Application : GLib.Application { ChatInteraction.start(stream_interactor); FileManager.start(stream_interactor, db); NotificationEvents.start(stream_interactor); + ContentItemAccumulator.start(stream_interactor); create_actions(); diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index e2542e74..93ba782f 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -23,7 +23,21 @@ public class FileTransfer : Object { public DateTime? local_time { get; set; } public Encryption encryption { get; set; } - public InputStream input_stream { get; set; } + private InputStream? input_stream_ = null; + public InputStream input_stream { + get { + if (input_stream_ == null) { + File file = File.new_for_path(Path.build_filename(storage_dir, path ?? file_name)); + try { + input_stream_ = file.read(); + } catch (Error e) { } + } + return input_stream_; + } + set { + input_stream_ = value; + } + } public OutputStream output_stream { get; set; } public string file_name { get; set; } @@ -41,9 +55,11 @@ public class FileTransfer : Object { public string info { get; set; } private Database? db; + private string storage_dir; - public FileTransfer.from_row(Database db, Qlite.Row row) { + public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) { this.db = db; + this.storage_dir = storage_dir; id = row[db.file_transfer.id]; account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO don’t have to generate acc new diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 62260076..2378feb7 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -75,15 +75,16 @@ public interface ConversationTitlebarWidget : Object { public abstract interface ConversationItemPopulator : Object { public abstract string id { get; } public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type); - public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } - public virtual void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } public abstract void close(Conversation conversation); } +public abstract interface ConversationAdditionPopulator : ConversationItemPopulator { + public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } +} + public abstract class MetaConversationItem : Object { + public virtual string populator_id { get; set; } public virtual Jid? jid { get; set; default=null; } - public virtual string color { get; set; default=null; } - public virtual string display_name { get; set; default=null; } public virtual bool dim { get; set; default=false; } public virtual DateTime? sort_time { get; set; default=null; } public virtual double seccondary_sort_indicator { get; set; } @@ -103,21 +104,4 @@ public interface ConversationItemCollection : Object { public signal void remove_item(MetaConversationItem item); } -public interface MessageDisplayProvider : Object { - public abstract string id { get; set; } - public abstract double priority { get; set; } - public abstract bool can_display(Entities.Message? message); - public abstract MetaConversationItem? get_item(Entities.Message message, Entities.Conversation conversation); -} - -public interface FileWidget : Object { - public abstract Object? get_widget(WidgetType type); -} - -public interface FileDisplayProvider : Object { - public abstract double priority { get; } - public abstract bool can_display(Entities.Message? message); - public abstract FileWidget? get_item(Entities.Message? message); -} - } diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala index 7b4410aa..2b496288 100644 --- a/libdino/src/plugin/registry.vala +++ b/libdino/src/plugin/registry.vala @@ -7,8 +7,7 @@ public class Registry { internal ArrayList account_settings_entries = new ArrayList(); internal ArrayList contact_details_entries = new ArrayList(); internal Map text_commands = new HashMap(); - internal Gee.List message_displays = new ArrayList(); - internal Gee.List conversation_item_populators = new ArrayList(); + internal Gee.List conversation_addition_populators = new ArrayList(); internal Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { if (a.order < b.order) { return -1; @@ -70,22 +69,12 @@ public class Registry { } } - public bool register_message_display(MessageDisplayProvider provider) { - lock (message_displays) { - foreach(MessageDisplayProvider p in message_displays) { - if (p.id == provider.id) return false; - } - message_displays.add(provider); - return true; - } - } - - public bool register_conversation_item_populator(ConversationItemPopulator populator) { - lock (conversation_item_populators) { - foreach(ConversationItemPopulator p in conversation_item_populators) { + public bool register_conversation_addition_populator(ConversationAdditionPopulator populator) { + lock (conversation_addition_populators) { + foreach(ConversationItemPopulator p in conversation_addition_populators) { if (p.id == populator.id) return false; } - conversation_item_populators.add(populator); + conversation_addition_populators.add(populator); return true; } } diff --git a/libdino/src/service/content_item_accumulator.vala b/libdino/src/service/content_item_accumulator.vala new file mode 100644 index 00000000..9fc852b2 --- /dev/null +++ b/libdino/src/service/content_item_accumulator.vala @@ -0,0 +1,224 @@ +using Gee; + +using Dino.Entities; +using Xmpp; + +namespace Dino { + +public class ContentItemAccumulator : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("content_item_accumulator"); + public string id { get { return IDENTITY.id; } } + + public signal void new_item(); + + private StreamInteractor stream_interactor; + private Gee.List filters = new ArrayList(); + private HashMap collection_conversations = new HashMap(); + + public static void start(StreamInteractor stream_interactor) { + ContentItemAccumulator m = new ContentItemAccumulator(stream_interactor); + stream_interactor.add_module(m); + } + + public ContentItemAccumulator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(on_new_message); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(on_new_message); + stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer); + } + + public void init(Conversation conversation, ContentItemCollection item_collection) { + collection_conversations[item_collection] = conversation; + } + + public Gee.List populate_latest(ContentItemCollection item_collection, Conversation conversation, int n) { + Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); + + Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation, n); + if (messages != null) { + foreach (Entities.Message message in messages) { + items.add(new MessageItem(message, conversation)); + } + } + Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_latest_transfers(conversation.account, conversation.counterpart, n); + foreach (FileTransfer transfer in transfers) { + items.add(new FileItem(transfer)); + } + + BidirIterator iter = items.bidir_iterator(); + iter.last(); + int i = 0; + while (i < n && iter.has_previous()) { + iter.previous(); + i++; + } + Gee.List ret = new ArrayList(); + do { + ret.add(iter.get()); + } while(iter.next()); + return ret; + } + + public Gee.List populate_before(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { + Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); + + Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, n); + if (messages != null) { + foreach (Entities.Message message in messages) { + items.add(new MessageItem(message, conversation)); + } + } + Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.display_time, n); + foreach (FileTransfer transfer in transfers) { + items.add(new FileItem(transfer)); + } + + BidirIterator iter = items.bidir_iterator(); + iter.last(); + int i = 0; + while (i < n && iter.has_previous()) { + iter.previous(); + i++; + } + Gee.List ret = new ArrayList(); + do { + ret.add(iter.get()); + } while(iter.next()); + return ret; + } + + public void populate_after(Conversation conversation, ContentItem item, int n) { + + } + + public void add_filter(ContentFilter content_filter) { + filters.add(content_filter); + } + + private void on_new_message(Message message, Conversation conversation) { + foreach (ContentItemCollection collection in collection_conversations.keys) { + if (collection_conversations[collection].equals(conversation)) { + MessageItem item = new MessageItem(message, conversation); + insert_item(collection, item); + } + } + } + + private void insert_file_transfer(FileTransfer file_transfer) { + foreach (ContentItemCollection collection in collection_conversations.keys) { + Conversation conversation = collection_conversations[collection]; + if (conversation.account.equals(file_transfer.account) && conversation.counterpart.equals_bare(file_transfer.counterpart)) { + FileItem item = new FileItem(file_transfer); + insert_item(collection, item); + } + } + } + + private void insert_item(ContentItemCollection item_collection, ContentItem content_item) { + bool insert = true; + foreach (ContentFilter filter in filters) { + if (filter.discard(content_item)) { + insert = false; + } + } + if (insert) { + item_collection.insert_item(content_item); + } + } +} + +public interface ContentItemCollection : Object { + public abstract void insert_item(ContentItem item); + public abstract void remove_item(ContentItem item); +} + +public interface ContentFilter : Object { + public abstract bool discard(ContentItem content_item); +} + +public abstract class ContentItem : Object { + public virtual string type_ { get; set; } + public virtual Jid? jid { get; set; default=null; } + public virtual DateTime? sort_time { get; set; default=null; } + public virtual double seccondary_sort_indicator { get; set; } + public virtual DateTime? display_time { get; set; default=null; } + public virtual Encryption? encryption { get; set; default=null; } + public virtual Entities.Message.Marked? mark { get; set; default=null; } + + public static int compare(ContentItem a, ContentItem b) { + int res = a.sort_time.compare(b.sort_time); + if (res == 0) { + res = a.display_time.compare(b.display_time); + } + if (res == 0) { + res = a.seccondary_sort_indicator - b.seccondary_sort_indicator > 0 ? 1 : -1; + } + return res; + } +} + +public class MessageItem : ContentItem { + public const string TYPE = "message"; + public override string type_ { get; set; default=TYPE; } + + public Message message; + public Conversation conversation; + + public MessageItem(Message message, Conversation conversation) { + this.message = message; + this.conversation = conversation; + + 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; + this.mark = message.marked; + + WeakRef weak_message = WeakRef(message); + message.notify["marked"].connect(() => { + Message? m = weak_message.get() as Message; + if (m == null) return; + mark = m.marked; + }); + } +} + +public class FileItem : ContentItem { + public const string TYPE = "file"; + public override string type_ { get; set; default=TYPE; } + + public FileTransfer file_transfer; + public Conversation conversation; + + public FileItem(FileTransfer file_transfer) { + 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); + }); + } + + 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/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 3def24af..667076dd 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -78,6 +78,37 @@ public class FileManager : StreamInteractionModule, Object { return false; } + public Gee.List get_latest_transfers(Account account, Jid counterpart, int n) { + Qlite.QueryBuilder select = db.file_transfer.select() + .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) + .with(db.file_transfer.account_id, "=", account.id) + .order_by(db.file_transfer.local_time, "DESC") + .limit(n); + + Gee.List ret = new ArrayList(); + foreach (Qlite.Row row in select) { + FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); + ret.insert(0, file_transfer); + } + return ret; + } + + public Gee.List get_transfers_before(Account account, Jid counterpart, DateTime before, int n) { + Qlite.QueryBuilder select = db.file_transfer.select() + .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) + .with(db.file_transfer.account_id, "=", account.id) + .with(db.file_transfer.local_time, "<", (long)before.to_unix()) + .order_by(db.file_transfer.local_time, "DESC") + .limit(n); + + Gee.List ret = new ArrayList(); + foreach (Qlite.Row row in select) { + FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); + ret.insert(0, file_transfer); + } + return ret; + } + public Gee.List get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) { Qlite.QueryBuilder select = db.file_transfer.select() .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) @@ -88,11 +119,7 @@ public class FileManager : StreamInteractionModule, Object { Gee.List ret = new ArrayList(); foreach (Qlite.Row row in select) { - FileTransfer file_transfer = new FileTransfer.from_row(db, row); - File file = File.new_for_path(Path.build_filename(get_storage_dir(), file_transfer.path ?? file_transfer.file_name)); - try { - file_transfer.input_stream = file.read(); - } catch (Error e) { } + FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); ret.insert(0, file_transfer); } return ret; diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index 35e05074..906693a3 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -51,23 +51,23 @@ public class MessageStorage : StreamInteractionModule, Object { return null; } - public Gee.List? get_messages_before_message(Conversation? conversation, Message message, int count = 20) { - SortedSet? before = messages[conversation].head_set(message); - if (before != null && before.size >= count) { - Gee.List ret = new ArrayList(Message.equals_func); - Iterator iter = before.iterator(); - iter.next(); - for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next(); - while(iter.has_next()) { - Message m = iter.get(); - ret.add(m); - iter.next(); - } - return ret; - } else { - Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, message.local_time); + public Gee.List? get_messages_before_message(Conversation? conversation, DateTime before, int count = 20) { +// SortedSet? before = messages[conversation].head_set(message); +// if (before != null && before.size >= count) { +// Gee.List ret = new ArrayList(Message.equals_func); +// Iterator iter = before.iterator(); +// iter.next(); +// for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next(); +// while(iter.has_next()) { +// Message m = iter.get(); +// ret.add(m); +// iter.next(); +// } +// return ret; +// } else { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before); return db_messages; - } +// } } public Message? get_message_by_id(string stanza_id, Conversation conversation) { 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 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? 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 generators = new HashMap(); + + 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("" + _("File") + ": " + file_transfer.mime_type + "") { 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..9fb83419 --- /dev/null +++ b/main/src/ui/conversation_summary/content_populator.vala @@ -0,0 +1,99 @@ +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) { + current_conversation = conversation; + this.item_collection = item_collection; + stream_interactor.get_module(ContentItemAccumulator.IDENTITY).init(conversation, this); + } + + public void close(Conversation conversation) { } + + public void insert_item(ContentItem item) { + item_collection.insert_item(new ContentMetaItem(item, widget_factory)); + } + + public void remove_item(ContentItem item) { } + + + public Gee.List populate_latest(Conversation conversation, int n) { + Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_latest(this, conversation, n); + Gee.List ret = new ArrayList(); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public Gee.List populate_before(Conversation conversation, Plugins.MetaConversationItem before_item, int n) { + Gee.List ret = new ArrayList(); + ContentMetaItem? content_meta_item = before_item as ContentMetaItem; + if (content_meta_item != null) { + Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_before(this, conversation, content_meta_item.content_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + } + return ret; + } +} + +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_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index fac53b7d..bb696572 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -19,11 +19,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private StreamInteractor stream_interactor; private Gee.TreeSet meta_items = new TreeSet(sort_meta_items); - private Gee.Map> meta_after_items = new Gee.HashMap>(); private Gee.HashMap item_item_skeletons = new Gee.HashMap(); private Gee.HashMap widgets = new Gee.HashMap(); private Gee.List item_skeletons = new Gee.ArrayList(); - private MessagePopulator message_item_populator; + private ContentProvider content_populator; private SubscriptionNotitication subscription_notification; private double? was_value; @@ -39,16 +38,15 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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); 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) { @@ -78,7 +76,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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); } } @@ -90,11 +88,14 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { animate = false; Timeout.add(20, () => { animate = true; return false; }); - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.init(conversation, this, Plugins.WidgetType.GTK); } - message_item_populator.init(conversation, this); - message_item_populator.populate_latest(conversation, 40); + content_populator.init(this, conversation, Plugins.WidgetType.GTK); + Gee.List items = content_populator.populate_latest(conversation, 40); + foreach (ContentMetaItem item in items) { + on_insert_item(item); + } Idle.add(() => { on_value_notify(); return false; }); subscription_notification.init(conversation, this); @@ -110,7 +111,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } } - public void on_remove_item(Plugins.MetaConversationItem item) { + private void on_remove_item(Plugins.MetaConversationItem item) { lock (meta_items) { ConversationItemSkeleton? skeleton = item_item_skeletons[item]; if (skeleton.items.size > 1) { @@ -202,11 +203,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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); } } @@ -253,7 +254,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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 items = content_populator.populate_before(conversation, meta_items.first(), 20); + foreach (ContentMetaItem item in items) { + on_insert_item(item); + } + } } private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { @@ -276,7 +282,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private void 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("" + _("File") + ": " + file_transfer.mime_type + "") { 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 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 meta_message = new HashMap(); - - 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? 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? 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..0c38c269 100644 --- a/main/src/ui/conversation_summary/message_textview.vala +++ b/main/src/ui/conversation_summary/message_textview.vala @@ -60,7 +60,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 +125,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/plugins/http-files/src/manager.vala b/plugins/http-files/src/manager.vala index 7335b89a..78c244e3 100644 --- a/plugins/http-files/src/manager.vala +++ b/plugins/http-files/src/manager.vala @@ -81,22 +81,19 @@ public class Manager : StreamInteractionModule, FileSender, Object { } } -public class FileMessageFilterDisplay : Plugins.MessageDisplayProvider, Object { - public string id { get; set; default="file_message_filter"; } - public double priority { get; set; default=10; } - +public class FileMessageFilter : ContentFilter, Object { public Database db; - public FileMessageFilterDisplay(Dino.Database db) { + public FileMessageFilter(Dino.Database db) { this.db = db; } - public bool can_display(Entities.Message? message) { - return message_is_file(db, message); - } - - public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) { - return null; + public bool discard(ContentItem content_item) { + if (content_item.type_ == MessageItem.TYPE) { + MessageItem message_item = content_item as MessageItem; + return message_is_file(db, message_item.message); + } + return false; } } diff --git a/plugins/http-files/src/plugin.vala b/plugins/http-files/src/plugin.vala index 1fc0c9fd..c57ee3dc 100644 --- a/plugins/http-files/src/plugin.vala +++ b/plugins/http-files/src/plugin.vala @@ -19,7 +19,7 @@ public class Plugin : RootInterface, Object { }); app.stream_interactor.get_module(FileManager.IDENTITY).add_provider(file_provider); - app.plugin_registry.register_message_display(new FileMessageFilterDisplay(app.db)); + app.stream_interactor.get_module(ContentItemAccumulator.IDENTITY).add_filter(new FileMessageFilter(app.db)); } public void shutdown() { From 443e7ee49da305a9ae8052c5b7a73412d03ce75f Mon Sep 17 00:00:00 2001 From: bobufa Date: Sat, 23 Jun 2018 11:57:39 +0200 Subject: [PATCH 04/21] fix local_time field of message and file_transfer being loaded from time db column --- libdino/src/entity/file_transfer.vala | 2 +- libdino/src/entity/message.vala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index 93ba782f..be472796 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -77,7 +77,7 @@ public class FileTransfer : Object { } direction = row[db.file_transfer.direction]; time = new DateTime.from_unix_utc(row[db.file_transfer.time]); - local_time = new DateTime.from_unix_utc(row[db.file_transfer.time]); + local_time = new DateTime.from_unix_utc(row[db.file_transfer.local_time]); encryption = (Encryption) row[db.file_transfer.encryption]; file_name = row[db.file_transfer.file_name]; path = row[db.file_transfer.path]; diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index 6e34e458..ac54a7c2 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -82,7 +82,7 @@ public class Message : Object { } direction = row[db.message.direction]; time = new DateTime.from_unix_utc(row[db.message.time]); - local_time = new DateTime.from_unix_utc(row[db.message.time]); + local_time = new DateTime.from_unix_utc(row[db.message.local_time]); body = row[db.message.body]; marked = (Message.Marked) row[db.message.marked]; encryption = (Encryption) row[db.message.encryption]; From 760fd4cb26340165fb85c2e3aee8390e46dc8b63 Mon Sep 17 00:00:00 2001 From: bobufa Date: Sat, 23 Jun 2018 11:59:21 +0200 Subject: [PATCH 05/21] load+display later messages when scrolling down --- .../src/service/content_item_accumulator.vala | 43 +++++-- libdino/src/service/database.vala | 14 ++- libdino/src/service/file_manager.vala | 24 ++-- libdino/src/service/message_storage.vala | 11 +- .../content_populator.vala | 12 ++ .../conversation_view.vala | 110 ++++++++++++------ 6 files changed, 147 insertions(+), 67 deletions(-) diff --git a/libdino/src/service/content_item_accumulator.vala b/libdino/src/service/content_item_accumulator.vala index 9fc852b2..9f9e672c 100644 --- a/libdino/src/service/content_item_accumulator.vala +++ b/libdino/src/service/content_item_accumulator.vala @@ -46,50 +46,73 @@ public class ContentItemAccumulator : StreamInteractionModule, Object { items.add(new FileItem(transfer)); } + Gee.List ret = new ArrayList(); + if (items.size == 0) return ret; + BidirIterator iter = items.bidir_iterator(); iter.last(); int i = 0; - while (i < n && iter.has_previous()) { + while (i < n - 1 && iter.has_previous()) { iter.previous(); i++; } - Gee.List ret = new ArrayList(); do { ret.add(iter.get()); - } while(iter.next()); + } while (iter.next()); return ret; } public Gee.List populate_before(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); - Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, n); + int before_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1; + Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, before_id, n); if (messages != null) { foreach (Entities.Message message in messages) { items.add(new MessageItem(message, conversation)); } } - Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.display_time, n); + Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.sort_time, n); foreach (FileTransfer transfer in transfers) { items.add(new FileItem(transfer)); } + Gee.List ret = new ArrayList(); + if (items.size == 0) return ret; + BidirIterator iter = items.bidir_iterator(); iter.last(); int i = 0; - while (i < n && iter.has_previous()) { + while (i < n - 1 && iter.has_previous()) { iter.previous(); i++; } - Gee.List ret = new ArrayList(); do { ret.add(iter.get()); - } while(iter.next()); + } while (iter.next()); return ret; } - public void populate_after(Conversation conversation, ContentItem item, int n) { + public Gee.List populate_after(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { + Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); + int after_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1; + Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(conversation, item.sort_time, after_id, n); + if (messages != null) { + foreach (Entities.Message message in messages) { + items.add(new MessageItem(message, conversation)); + } + } + Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_after(conversation.account, conversation.counterpart, item.sort_time, n); + foreach (FileTransfer transfer in transfers) { + items.add(new FileItem(transfer)); + } + + Gee.List ret = new ArrayList(); + foreach (ContentItem content_item in items) { + ret.add(content_item); + } + return ret; } public void add_filter(ContentFilter content_filter) { @@ -196,7 +219,7 @@ public class FileItem : ContentItem { 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.sort_time = file_transfer.local_time; this.seccondary_sort_indicator = file_transfer.id + 0.2903; this.display_time = file_transfer.time; this.encryption = file_transfer.encryption; diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 2dca686f..d02e4c71 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -236,11 +236,10 @@ public class Database : Qlite.Database { } } - public Gee.List get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before) { + public Gee.List get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) { QueryBuilder select = message.select() .with(message.counterpart_id, "=", get_jid_id(jid)) .with(message.account_id, "=", account.id) - .order_by(message.id, "DESC") .limit(count); if (jid.resourcepart != null) { select.with(message.counterpart_resource, "=", jid.resourcepart); @@ -250,6 +249,17 @@ public class Database : Qlite.Database { } if (before != null) { select.with(message.local_time, "<", (long) before.to_unix()); + if (id > 0) { + select.with(message.id, "<", id); + } + } + if (after != null) { + select.with(message.local_time, ">", (long) after.to_unix()); + if (id > 0) { + select.with(message.id, ">", id); + } + } else { + select.order_by(message.id, "DESC"); } LinkedList ret = new LinkedList(); diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 667076dd..18f1735d 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -84,13 +84,7 @@ public class FileManager : StreamInteractionModule, Object { .with(db.file_transfer.account_id, "=", account.id) .order_by(db.file_transfer.local_time, "DESC") .limit(n); - - Gee.List ret = new ArrayList(); - foreach (Qlite.Row row in select) { - FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); - ret.insert(0, file_transfer); - } - return ret; + return get_transfers_from_qry(select); } public Gee.List get_transfers_before(Account account, Jid counterpart, DateTime before, int n) { @@ -100,23 +94,19 @@ public class FileManager : StreamInteractionModule, Object { .with(db.file_transfer.local_time, "<", (long)before.to_unix()) .order_by(db.file_transfer.local_time, "DESC") .limit(n); - - Gee.List ret = new ArrayList(); - foreach (Qlite.Row row in select) { - FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); - ret.insert(0, file_transfer); - } - return ret; + return get_transfers_from_qry(select); } - public Gee.List get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) { + public Gee.List get_transfers_after(Account account, Jid counterpart, DateTime after, int n) { Qlite.QueryBuilder select = db.file_transfer.select() .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) .with(db.file_transfer.account_id, "=", account.id) .with(db.file_transfer.local_time, ">", (long)after.to_unix()) - .with(db.file_transfer.local_time, "<", (long)before.to_unix()) - .order_by(db.file_transfer.id, "DESC"); + .limit(n); + return get_transfers_from_qry(select); + } + private Gee.List get_transfers_from_qry(Qlite.QueryBuilder select) { Gee.List ret = new ArrayList(); foreach (Qlite.Row row in select) { FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index 906693a3..e3869e41 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -51,7 +51,7 @@ public class MessageStorage : StreamInteractionModule, Object { return null; } - public Gee.List? get_messages_before_message(Conversation? conversation, DateTime before, int count = 20) { + public Gee.List? get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) { // SortedSet? before = messages[conversation].head_set(message); // if (before != null && before.size >= count) { // Gee.List ret = new ArrayList(Message.equals_func); @@ -65,11 +65,16 @@ public class MessageStorage : StreamInteractionModule, Object { // } // return ret; // } else { - Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before); + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id); return db_messages; // } } + public Gee.List? get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) { + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id); + return db_messages; + } + public Message? get_message_by_id(string stanza_id, Conversation conversation) { init_conversation(conversation); foreach (Message message in messages[conversation]) { @@ -100,7 +105,7 @@ public class MessageStorage : StreamInteractionModule, Object { } return res; }); - Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null); + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null, null, -1); messages[conversation].add_all(db_messages); } } diff --git a/main/src/ui/conversation_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala index 9fb83419..cec54c7b 100644 --- a/main/src/ui/conversation_summary/content_populator.vala +++ b/main/src/ui/conversation_summary/content_populator.vala @@ -53,6 +53,18 @@ public class ContentProvider : ContentItemCollection, Object { } return ret; } + + public Gee.List populate_after(Conversation conversation, Plugins.MetaConversationItem before_item, int n) { + Gee.List ret = new ArrayList(); + ContentMetaItem? content_meta_item = before_item as ContentMetaItem; + if (content_meta_item != null) { + Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_after(this, conversation, content_meta_item.content_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + } + return ret; + } } public class ContentMetaItem : Plugins.MetaConversationItem { diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index bb696572..008909e4 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -18,7 +18,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { [GtkChild] private Stack stack; private StreamInteractor stream_interactor; - private Gee.TreeSet meta_items = new TreeSet(sort_meta_items); + private Gee.TreeSet content_items = new Gee.TreeSet(compare_meta_items); + private Gee.TreeSet meta_items = new TreeSet(compare_meta_items); private Gee.HashMap item_item_skeletons = new Gee.HashMap(); private Gee.HashMap widgets = new Gee.HashMap(); private Gee.List item_skeletons = new Gee.ArrayList(); @@ -32,6 +33,7 @@ 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; public ConversationView(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; @@ -41,8 +43,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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(do_insert_item); + remove_item.connect(do_remove_item); Application app = GLib.Application.get_default() as Application; app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); @@ -82,49 +84,57 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } this.conversation = conversation; stack.set_visible_child_name("void"); + + 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); + + display_latest(); + + stack.set_visible_child_name("main"); + } + + private void display_latest() { clear(); was_upper = null; was_page_size = null; animate = false; Timeout.add(20, () => { animate = true; return false; }); - 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); Gee.List items = content_populator.populate_latest(conversation, 40); foreach (ContentMetaItem item in items) { - on_insert_item(item); + do_insert_item(item); } Idle.add(() => { on_value_notify(); return false; }); - - subscription_notification.init(conversation, this); - - stack.set_visible_child_name("main"); } - 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); } - private void on_remove_item(Plugins.MetaConversationItem item) { - lock (meta_items) { - ConversationItemSkeleton? skeleton = item_item_skeletons[item]; - if (skeleton.items.size > 1) { - skeleton.remove_meta_item(item); - } else { - widgets[item].destroy(); - widgets.unset(item); - skeleton.destroy(); - item_skeletons.remove(skeleton); - item_item_skeletons.unset(item); - } - meta_items.remove(item); + private void do_remove_item(Plugins.MetaConversationItem item) { + ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + if (skeleton.items.size > 1) { + skeleton.remove_meta_item(item); + } else { + widgets[item].destroy(); + widgets.unset(item); + skeleton.destroy(); + item_skeletons.remove(skeleton); + item_item_skeletons.unset(item); } + content_items.remove(item); + meta_items.remove(item); } public void add_notification(Widget widget) { @@ -154,8 +164,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { lower_skeleton.add_meta_item(item); 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; } @@ -182,7 +192,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; @@ -220,12 +229,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++; @@ -235,19 +244,24 @@ 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 (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(); } } @@ -255,14 +269,39 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { was_value = scrolled.vadjustment.value; if (!reloading_mutex.trylock()) return; if (meta_items.size > 0) { - Gee.List items = content_populator.populate_before(conversation, meta_items.first(), 20); + Gee.List items = content_populator.populate_before(conversation, content_items.first(), 20); foreach (ContentMetaItem item in items) { - on_insert_item(item); + 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) { + foreach (Plugins.MetaConversationItem a in content_items) { + ContentMetaItem b = a as ContentMetaItem; + MessageItem c = b.content_item as MessageItem; + } + Gee.List items = content_populator.populate_after(conversation, content_items.last(), 20); + + ContentMetaItem b = content_items.last() as ContentMetaItem; + MessageItem c = b.content_item as MessageItem; + + 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; @@ -281,6 +320,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } private void clear() { + content_items.clear(); meta_items.clear(); item_skeletons.clear(); item_item_skeletons.clear(); From babfc3bd36e0cfa50f06648224f33a6a96eb27ea Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 27 Jun 2018 16:58:10 +0200 Subject: [PATCH 06/21] qlite: add basic (outer) join functionality --- qlite/src/column.vala | 57 ++++++++++++++++++--------------- qlite/src/query_builder.vala | 45 ++++++++++++++++++-------- qlite/src/row.vala | 61 +++++++++++++++++++++++++++--------- qlite/src/table.vala | 10 ++++-- 4 files changed, 118 insertions(+), 55 deletions(-) diff --git a/qlite/src/column.vala b/qlite/src/column.vala index 9c201885..daa6a59f 100644 --- a/qlite/src/column.vala +++ b/qlite/src/column.vala @@ -3,6 +3,8 @@ using Sqlite; namespace Qlite { public abstract class Column { + public const string DEFALT_TABLE_NAME = ""; + public string name { get; private set; } public string? default { get; set; } public int sqlite_type { get; private set; } @@ -12,16 +14,21 @@ public abstract class Column { public virtual bool not_null { get; set; } public long min_version { get; set; default = -1; } public long max_version { get; set; default = long.MAX; } + internal Table table { get; set; } - public abstract T get(Row row); + public abstract T get(Row row, string? table_name = DEFALT_TABLE_NAME); - public virtual bool is_null(Row row) { + public virtual bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { return false; } internal abstract void bind(Statement stmt, int index, T value); public string to_string() { + return table == null ? name : (table.name + "." + name); + } + + public string to_column_definition() { string res = name; switch (sqlite_type) { case INTEGER: @@ -58,12 +65,12 @@ public abstract class Column { base(name, INTEGER); } - public override int get(Row row) { - return (int) row.get_integer(name); + public override int get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return (int) row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } - public override bool is_null(Row row) { - return !row.has_integer(name); + public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { + return !row.has_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } internal override void bind(Statement stmt, int index, int value) { @@ -76,12 +83,12 @@ public abstract class Column { base(name, INTEGER); } - public override long get(Row row) { - return (long) row.get_integer(name); + public override long get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return (long) row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } - public override bool is_null(Row row) { - return !row.has_integer(name); + public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { + return !row.has_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } internal override void bind(Statement stmt, int index, long value) { @@ -94,12 +101,12 @@ public abstract class Column { base(name, FLOAT); } - public override double get(Row row) { - return row.get_real(name); + public override double get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return row.get_real(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } - public override bool is_null(Row row) { - return !row.has_real(name); + public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { + return !row.has_real(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } internal override void bind(Statement stmt, int index, double value) { @@ -112,12 +119,12 @@ public abstract class Column { base(name, TEXT); } - public override string? get(Row row) { - return row.get_text(name); + public override string? get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } - public override bool is_null(Row row) { - return get(row) == null; + public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { + return get(row, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == null; } internal override void bind(Statement stmt, int index, string? value) { @@ -136,11 +143,11 @@ public abstract class Column { public override bool not_null { get { return true; } set {} } - public override string get(Row row) { - return (!)row.get_text(name); + public override string get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return (!)row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name); } - public override bool is_null(Row row) { + public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) { return false; } @@ -154,8 +161,8 @@ public abstract class Column { base(name, TEXT); } - public override bool get(Row row) { - return row.get_text(name) == "1"; + public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == "1"; } internal override void bind(Statement stmt, int index, bool value) { @@ -168,8 +175,8 @@ public abstract class Column { base(name, INTEGER); } - public override bool get(Row row) { - return row.get_integer(name) == 1; + public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) { + return row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == 1; } internal override void bind(Statement stmt, int index, bool value) { diff --git a/qlite/src/query_builder.vala b/qlite/src/query_builder.vala index 9ca5bf25..d1254b53 100644 --- a/qlite/src/query_builder.vala +++ b/qlite/src/query_builder.vala @@ -33,12 +33,12 @@ public class QueryBuilder : StatementBuilder { public QueryBuilder select(Column[] columns = {}) { this.columns = columns; - if (columns.length == 0) { + if (columns.length != 0) { for (int i = 0; i < columns.length; i++) { if (column_selector == "*") { - column_selector = columns[0].name; + column_selector = columns[i].to_string(); } else { - column_selector += ", " + columns[i].name; + column_selector += ", " + columns[i].to_string(); } } } else { @@ -65,14 +65,33 @@ public class QueryBuilder : StatementBuilder { return this; } - public QueryBuilder join(string table, string on) { - joins += @"JOIN $table ON $on"; + public QueryBuilder outer_join_with(Table table, Column lhs, Column rhs, string? as = null) { + return outer_join_on(table, @"$lhs = $rhs", as); + } + + public QueryBuilder outer_join_on(Table table, string on, string? as = null) { + if (as == null) as = table.name; + joins += @" LEFT OUTER JOIN $(table.name) AS $as ON $on"; + return this; + } + + public QueryBuilder join_with(Table table, Column lhs, Column rhs, string? as = null) { + return join_on(table, @"$lhs = $rhs", as); + } + + public QueryBuilder join_on(Table table, string on, string? as = null) { + if (as == null) as = table.name; + joins += @" JOIN $(table.name) AS $as ON $on"; + return this; + } + + internal QueryBuilder join_name(string table_name, string on) { + joins += @" JOIN $table_name ON $on"; return this; } public QueryBuilder where(string selection, string[] selection_args = {}) { - if (this.selection != "1") error("selection was already done, but where() was called."); - this.selection = selection; + this.selection = @"($(this.selection)) AND ($selection)"; foreach (string arg in selection_args) { this.selection_args += new StatementBuilder.StringField(arg); } @@ -82,17 +101,17 @@ public class QueryBuilder : StatementBuilder { public QueryBuilder with(Column column, string comp, T value) { if ((column.unique || column.primary_key) && comp == "=") single_result = true; selection_args += new Field(column, value); - selection = @"($selection) AND $table_name.$(column.name) $comp ?"; + selection = @"($selection) AND $column $comp ?"; return this; } public QueryBuilder with_null(Column column) { - selection = @"($selection) AND $table_name.$(column.name) ISNULL"; + selection = @"($selection) AND $column ISNULL"; return this; } public QueryBuilder without_null(Column column) { - selection = @"($selection) AND $table_name.$(column.name) NOT NULL"; + selection = @"($selection) AND $column NOT NULL"; return this; } @@ -155,13 +174,13 @@ public class QueryBuilder : StatementBuilder { } class OrderingTerm { - Column column; + Column? column; string column_name; string dir; public OrderingTerm(Column column, string dir) { this.column = column; - this.column_name = column.name; + this.column_name = column.to_string(); this.dir = dir; } @@ -190,7 +209,7 @@ public class MatchQueryBuilder : QueryBuilder { base(db); if (table.fts_columns == null) error("MATCH query on non FTS table"); from(table); - join(@"_fts_$table_name", @"_fts_$table_name.docid = $table_name.rowid"); + join_name(@"_fts_$table_name", @"_fts_$table_name.docid = $table_name.rowid"); } public MatchQueryBuilder match(Column column, string match) { diff --git a/qlite/src/row.vala b/qlite/src/row.vala index be459719..d3807f41 100644 --- a/qlite/src/row.vala +++ b/qlite/src/row.vala @@ -10,15 +10,21 @@ public class Row { internal Row(Statement stmt) { for (int i = 0; i < stmt.column_count(); i++) { + string column_name; + if (stmt.column_origin_name(i) != null) { + column_name = @"$(stmt.column_table_name(i)).$(stmt.column_origin_name(i))"; + } else { + column_name = stmt.column_name(i); + } switch(stmt.column_type(i)) { case TEXT: - text_map[stmt.column_name(i)] = stmt.column_text(i); + text_map[column_name] = stmt.column_text(i); break; case INTEGER: - int_map[stmt.column_name(i)] = (long) stmt.column_int64(i); + int_map[column_name] = (long) stmt.column_int64(i); break; case FLOAT: - real_map[stmt.column_name(i)] = stmt.column_double(i); + real_map[column_name] = stmt.column_double(i); break; } } @@ -28,27 +34,54 @@ public class Row { return field[this]; } - public string? get_text(string field) { - if (text_map.has_key(field)) { - return text_map[field]; + private string field_name(string field, string? table) { + if (table != null) { + return @"$table.$field"; + } else { + return field; + } + } + + public string? get_text(string field, string? table = null) { + if (text_map.has_key(field_name(field, table))) { + return text_map[field_name(field, table)]; } return null; } - public long get_integer(string field) { - return int_map[field]; + public long get_integer(string field, string? table = null) { + return int_map[field_name(field, table)]; } - public bool has_integer(string field) { - return int_map.has_key(field); + public bool has_integer(string field, string? table = null) { + return int_map.has_key(field_name(field, table)); } - public double get_real(string field, double def = 0) { - return real_map[field] ?? def; + public double get_real(string field, string? table = null, double def = 0) { + return real_map[field_name(field, table)] ?? def; } - public bool has_real(string field) { - return real_map.has_key(field) && real_map[field] != null; + public bool has_real(string field, string? table = null) { + return real_map.has_key(field_name(field, table)) && real_map[field_name(field, table)] != null; + } + + public string to_string() { + string ret = "{"; + + foreach (string key in text_map.keys) { + if (ret.length > 1) ret += ", "; + ret = @"$ret$key: \"$(text_map[key])\""; + } + foreach (string key in int_map.keys) { + if (ret.length > 1) ret += ", "; + ret = @"$ret$key: $(int_map[key])"; + } + foreach (string key in real_map.keys) { + if (ret.length > 1) ret += ", "; + ret = @"$ret$key: $(real_map[key])"; + } + + return ret + "}"; } } diff --git a/qlite/src/table.vala b/qlite/src/table.vala index 8725c4c9..607a396c 100644 --- a/qlite/src/table.vala +++ b/qlite/src/table.vala @@ -19,6 +19,10 @@ public class Table { public void init(Column[] columns, string constraints = "") { this.columns = columns; this.constraints = constraints; + + foreach(Column c in columns) { + c.table = this; + } } public void fts(Column[] columns) { @@ -28,7 +32,7 @@ public class Table { string cnames = ""; string cnews = ""; foreach (Column c in columns) { - cs += @", $c"; + cs += @", $(c.to_column_definition())"; cnames += @", $(c.name)"; cnews += @", new.$(c.name)"; } @@ -140,7 +144,7 @@ public class Table { for (int i = 0; i < columns.length; i++) { Column c = columns[i]; if (c.min_version <= version && c.max_version >= version) { - sql += @"$(i > 0 ? "," : "") $c"; + sql += @"$(i > 0 ? "," : "") $(c.to_column_definition())"; } } sql += @"$constraints)"; @@ -163,7 +167,7 @@ public class Table { foreach (Column c in columns) { if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version) { try { - db.exec(@"ALTER TABLE $name ADD COLUMN $c"); + db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())"); } catch (Error e) { error("Qlite Error: Add columns for version"); } From 8b23ddad2d33a1504cd28c0df583dfe50cadccda Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 4 Jul 2018 23:38:28 +0200 Subject: [PATCH 07/21] 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 +------- .../conversation_view.vala | 3 +- .../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 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() { From 61915ca56617e8f45ae8bd85cb87f0b8a9a895b0 Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 10 Jul 2018 00:31:39 +0200 Subject: [PATCH 08/21] 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 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"); } From 2e2a9a239000509488f1a369ea4eaf4cdda9c0b1 Mon Sep 17 00:00:00 2001 From: bobufa Date: Mon, 16 Jul 2018 21:26:39 +0200 Subject: [PATCH 09/21] accumulate conversation content in meta db table --- libdino/CMakeLists.txt | 2 +- libdino/src/application.vala | 2 +- .../src/service/content_item_accumulator.vala | 247 ------------------ libdino/src/service/content_item_store.vala | 246 +++++++++++++++++ .../counterpart_interaction_manager.vala | 6 +- libdino/src/service/database.vala | 48 +++- libdino/src/service/message_storage.vala | 10 +- .../content_populator.vala | 35 ++- .../conversation_view.vala | 8 +- plugins/http-files/src/plugin.vala | 2 +- 10 files changed, 330 insertions(+), 276 deletions(-) delete mode 100644 libdino/src/service/content_item_accumulator.vala create mode 100644 libdino/src/service/content_item_store.vala diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index de44195d..054e2bab 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -29,7 +29,7 @@ SOURCES src/service/blocking_manager.vala src/service/chat_interaction.vala src/service/connection_manager.vala - src/service/content_item_accumulator.vala + src/service/content_item_store.vala src/service/conversation_manager.vala src/service/counterpart_interaction_manager.vala src/service/database.vala diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 80e474ac..7f278fa0 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -38,7 +38,7 @@ public interface Dino.Application : GLib.Application { ChatInteraction.start(stream_interactor); FileManager.start(stream_interactor, db); NotificationEvents.start(stream_interactor); - ContentItemAccumulator.start(stream_interactor); + ContentItemStore.start(stream_interactor, db); SearchProcessor.start(stream_interactor, db); create_actions(); diff --git a/libdino/src/service/content_item_accumulator.vala b/libdino/src/service/content_item_accumulator.vala deleted file mode 100644 index 9f9e672c..00000000 --- a/libdino/src/service/content_item_accumulator.vala +++ /dev/null @@ -1,247 +0,0 @@ -using Gee; - -using Dino.Entities; -using Xmpp; - -namespace Dino { - -public class ContentItemAccumulator : StreamInteractionModule, Object { - public static ModuleIdentity IDENTITY = new ModuleIdentity("content_item_accumulator"); - public string id { get { return IDENTITY.id; } } - - public signal void new_item(); - - private StreamInteractor stream_interactor; - private Gee.List filters = new ArrayList(); - private HashMap collection_conversations = new HashMap(); - - public static void start(StreamInteractor stream_interactor) { - ContentItemAccumulator m = new ContentItemAccumulator(stream_interactor); - stream_interactor.add_module(m); - } - - public ContentItemAccumulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(on_new_message); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(on_new_message); - stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer); - } - - public void init(Conversation conversation, ContentItemCollection item_collection) { - collection_conversations[item_collection] = conversation; - } - - public Gee.List populate_latest(ContentItemCollection item_collection, Conversation conversation, int n) { - Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); - - Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation, n); - if (messages != null) { - foreach (Entities.Message message in messages) { - items.add(new MessageItem(message, conversation)); - } - } - Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_latest_transfers(conversation.account, conversation.counterpart, n); - foreach (FileTransfer transfer in transfers) { - items.add(new FileItem(transfer)); - } - - Gee.List ret = new ArrayList(); - if (items.size == 0) return ret; - - BidirIterator iter = items.bidir_iterator(); - iter.last(); - int i = 0; - while (i < n - 1 && iter.has_previous()) { - iter.previous(); - i++; - } - do { - ret.add(iter.get()); - } while (iter.next()); - return ret; - } - - public Gee.List populate_before(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { - Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); - - int before_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1; - Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, before_id, n); - if (messages != null) { - foreach (Entities.Message message in messages) { - items.add(new MessageItem(message, conversation)); - } - } - Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.sort_time, n); - foreach (FileTransfer transfer in transfers) { - items.add(new FileItem(transfer)); - } - - Gee.List ret = new ArrayList(); - if (items.size == 0) return ret; - - BidirIterator iter = items.bidir_iterator(); - iter.last(); - int i = 0; - while (i < n - 1 && iter.has_previous()) { - iter.previous(); - i++; - } - do { - ret.add(iter.get()); - } while (iter.next()); - return ret; - } - - public Gee.List populate_after(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { - Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); - - int after_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1; - Gee.List? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(conversation, item.sort_time, after_id, n); - if (messages != null) { - foreach (Entities.Message message in messages) { - items.add(new MessageItem(message, conversation)); - } - } - Gee.List transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_after(conversation.account, conversation.counterpart, item.sort_time, n); - foreach (FileTransfer transfer in transfers) { - items.add(new FileItem(transfer)); - } - - Gee.List ret = new ArrayList(); - foreach (ContentItem content_item in items) { - ret.add(content_item); - } - return ret; - } - - public void add_filter(ContentFilter content_filter) { - filters.add(content_filter); - } - - private void on_new_message(Message message, Conversation conversation) { - foreach (ContentItemCollection collection in collection_conversations.keys) { - if (collection_conversations[collection].equals(conversation)) { - MessageItem item = new MessageItem(message, conversation); - insert_item(collection, item); - } - } - } - - private void insert_file_transfer(FileTransfer file_transfer) { - foreach (ContentItemCollection collection in collection_conversations.keys) { - Conversation conversation = collection_conversations[collection]; - if (conversation.account.equals(file_transfer.account) && conversation.counterpart.equals_bare(file_transfer.counterpart)) { - FileItem item = new FileItem(file_transfer); - insert_item(collection, item); - } - } - } - - private void insert_item(ContentItemCollection item_collection, ContentItem content_item) { - bool insert = true; - foreach (ContentFilter filter in filters) { - if (filter.discard(content_item)) { - insert = false; - } - } - if (insert) { - item_collection.insert_item(content_item); - } - } -} - -public interface ContentItemCollection : Object { - public abstract void insert_item(ContentItem item); - public abstract void remove_item(ContentItem item); -} - -public interface ContentFilter : Object { - public abstract bool discard(ContentItem content_item); -} - -public abstract class ContentItem : Object { - public virtual string type_ { get; set; } - public virtual Jid? jid { get; set; default=null; } - public virtual DateTime? sort_time { get; set; default=null; } - public virtual double seccondary_sort_indicator { get; set; } - public virtual DateTime? display_time { get; set; default=null; } - public virtual Encryption? encryption { get; set; default=null; } - public virtual Entities.Message.Marked? mark { get; set; default=null; } - - public static int compare(ContentItem a, ContentItem b) { - int res = a.sort_time.compare(b.sort_time); - if (res == 0) { - res = a.display_time.compare(b.display_time); - } - if (res == 0) { - res = a.seccondary_sort_indicator - b.seccondary_sort_indicator > 0 ? 1 : -1; - } - return res; - } -} - -public class MessageItem : ContentItem { - public const string TYPE = "message"; - public override string type_ { get; set; default=TYPE; } - - public Message message; - public Conversation conversation; - - public MessageItem(Message message, Conversation conversation) { - this.message = message; - this.conversation = conversation; - - 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; - this.mark = message.marked; - - WeakRef weak_message = WeakRef(message); - message.notify["marked"].connect(() => { - Message? m = weak_message.get() as Message; - if (m == null) return; - mark = m.marked; - }); - } -} - -public class FileItem : ContentItem { - public const string TYPE = "file"; - public override string type_ { get; set; default=TYPE; } - - public FileTransfer file_transfer; - public Conversation conversation; - - public FileItem(FileTransfer file_transfer) { - 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.local_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); - }); - } - - 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/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala new file mode 100644 index 00000000..39bdfdde --- /dev/null +++ b/libdino/src/service/content_item_store.vala @@ -0,0 +1,246 @@ +using Gee; + +using Dino.Entities; +using Qlite; +using Xmpp; + +namespace Dino { + +public class ContentItemStore : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("content_item_store"); + public string id { get { return IDENTITY.id; } } + + public signal void new_item(ContentItem item, Conversation conversation); + + private StreamInteractor stream_interactor; + private Database db; + private Gee.List filters = new ArrayList(); + private HashMap collection_conversations = new HashMap(Conversation.hash_func, Conversation.equals_func); + + public static void start(StreamInteractor stream_interactor, Database db) { + ContentItemStore m = new ContentItemStore(stream_interactor, db); + stream_interactor.add_module(m); + } + + public ContentItemStore(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + + stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(on_new_message); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(on_new_message); + stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer); + } + + public void init(Conversation conversation, ContentItemCollection item_collection) { + collection_conversations[conversation] = item_collection; + } + + public void uninit(Conversation conversation, ContentItemCollection item_collection) { + collection_conversations.unset(conversation); + } + + public Gee.List get_items_from_query(QueryBuilder select, Conversation conversation) { + Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); + + foreach (var row in select) { + int provider = row[db.content.content_type]; + int foreign_id = row[db.content.foreign_id]; + switch (provider) { + case 1: + RowOption row_option = db.message.select().with(db.message.id, "=", foreign_id).row(); + if (row_option.is_present()) { + Message message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation); + if (message == null) { + message = new Message.from_row(db, row_option.inner); + } + items.add(new MessageItem(message, conversation, row[db.content.id])); + } + break; + case 2: + RowOption row_option = db.file_transfer.select().with(db.file_transfer.id, "=", foreign_id).row(); + if (row_option.is_present()) { + string storage_dir = stream_interactor.get_module(FileManager.IDENTITY).get_storage_dir(); + FileTransfer file_transfer = new FileTransfer.from_row(db, row_option.inner, storage_dir); + items.add(new FileItem(file_transfer, row[db.content.id])); + } + break; + } + } + + Gee.List ret = new ArrayList(); + foreach (ContentItem item in items) { + ret.add(item); + } + return ret; + } + + public Gee.List get_latest(Conversation conversation, int count) { + QueryBuilder select = db.content.select() + .with(db.content.conversation_id, "=", conversation.id) + .order_by(db.content.local_time, "DESC") + .order_by(db.content.time, "DESC") + .limit(count); + + return get_items_from_query(select, conversation); + } + + public Gee.List get_before(Conversation conversation, ContentItem item, int count) { + long local_time = (long) item.sort_time.to_unix(); + long time = (long) item.display_time.to_unix(); + QueryBuilder select = db.content.select() + .where(@"local_time < ? OR (local_time = ? AND time < ?) OR (local_time = ? AND time = ? AND id < ?)", { local_time.to_string(), local_time.to_string(), time.to_string(), local_time.to_string(), time.to_string(), item.id.to_string() }) + .with(db.content.conversation_id, "=", conversation.id) + .order_by(db.content.local_time, "DESC") + .order_by(db.content.time, "DESC") + .limit(count); + + return get_items_from_query(select, conversation); + } + + public Gee.List get_after(Conversation conversation, ContentItem item, int count) { + long local_time = (long) item.sort_time.to_unix(); + long time = (long) item.display_time.to_unix(); + QueryBuilder select = db.content.select() + .where(@"local_time > ? OR (local_time = ? AND time > ?) OR (local_time = ? AND time = ? AND id > ?)", { local_time.to_string(), local_time.to_string(), time.to_string(), local_time.to_string(), time.to_string(), item.id.to_string() }) + .with(db.content.conversation_id, "=", conversation.id) + .order_by(db.content.local_time, "ASC") + .order_by(db.content.time, "ASC") + .limit(count); + + return get_items_from_query(select, conversation); + } + + public void add_filter(ContentFilter content_filter) { + filters.add(content_filter); + } + + private void on_new_message(Message message, Conversation conversation) { + MessageItem item = new MessageItem(message, conversation, -1); + if (!discard(item)) { + item.id = db.add_content_item(conversation, message.time, message.local_time, 1, message.id); + + if (collection_conversations.has_key(conversation)) { + collection_conversations.get(conversation).insert_item(item); + } + new_item(item, conversation); + } + } + + private void insert_file_transfer(FileTransfer file_transfer) { + FileItem item = new FileItem(file_transfer, -1); + if (!discard(item)) { + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart, file_transfer.account); + item.id = db.add_content_item(conversation, file_transfer.time, file_transfer.local_time, 2, file_transfer.id); + + if (collection_conversations.has_key(conversation)) { + collection_conversations.get(conversation).insert_item(item); + } + new_item(item, conversation); + } + } + + private bool discard(ContentItem content_item) { + foreach (ContentFilter filter in filters) { + if (filter.discard(content_item)) { + return true; + } + } + return false; + } +} + +public interface ContentItemCollection : Object { + public abstract void insert_item(ContentItem item); + public abstract void remove_item(ContentItem item); +} + +public interface ContentFilter : Object { + public abstract bool discard(ContentItem content_item); +} + +public abstract class ContentItem : Object { + public int id { get; set; } + public string type_ { get; set; } + public Jid? jid { get; set; default=null; } + public DateTime? sort_time { get; set; default=null; } + public double seccondary_sort_indicator { get; set; } + public DateTime? display_time { get; set; default=null; } + public Encryption? encryption { get; set; default=null; } + public Entities.Message.Marked? mark { get; set; default=null; } + + public ContentItem(int id, string ty, Jid jid, DateTime sort_time, double seccondary_sort_indicator, DateTime display_time, Encryption encryption, Entities.Message.Marked mark) { + this.id = id; + this.type_ = ty; + this.jid = jid; + this.sort_time = sort_time; + this.seccondary_sort_indicator = seccondary_sort_indicator; + this.display_time = display_time; + this.encryption = encryption; + this.mark = mark; + } + + public static int compare(ContentItem a, ContentItem b) { + int res = a.sort_time.compare(b.sort_time); + if (res == 0) { + res = a.display_time.compare(b.display_time); + } + if (res == 0) { + res = a.seccondary_sort_indicator - b.seccondary_sort_indicator > 0 ? 1 : -1; + } + return res; + } +} + +public class MessageItem : ContentItem { + public const string TYPE = "message"; + + public Message message; + public Conversation conversation; + + public MessageItem(Message message, Conversation conversation, int id) { + base(id, TYPE, message.from, message.local_time, message.id + 0.0845, message.time, message.encryption, message.marked); + this.message = message; + this.conversation = conversation; + + WeakRef weak_message = WeakRef(message); + message.notify["marked"].connect(() => { + Message? m = weak_message.get() as Message; + if (m == null) return; + mark = m.marked; + }); + } +} + +public class FileItem : ContentItem { + public const string TYPE = "file"; + + public FileTransfer file_transfer; + public Conversation conversation; + + public FileItem(FileTransfer file_transfer, int id) { + Jid jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; + base(id, TYPE, jid, file_transfer.local_time, file_transfer.id + 0.0845, file_transfer.time, file_transfer.encryption, file_to_message_state(file_transfer.state)); + + this.file_transfer = file_transfer; + + file_transfer.notify["state"].connect_after(() => { + this.mark = file_to_message_state(file_transfer.state); + }); + } + + private static 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/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala index fb10d20c..b4df9b8d 100644 --- a/libdino/src/service/counterpart_interaction_manager.vala +++ b/libdino/src/service/counterpart_interaction_manager.vala @@ -9,7 +9,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object { public string id { get { return IDENTITY.id; } } public signal void received_state(Account account, Jid jid, string state); - public signal void received_marker(Account account, Jid jid, Entities.Message message, string marker); + public signal void received_marker(Account account, Jid jid, Entities.Message message, Entities.Message.Marked marker); public signal void received_message_received(Account account, Jid jid, Entities.Message message); public signal void received_message_displayed(Account account, Jid jid, Entities.Message message); @@ -69,12 +69,12 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object { if (marker != Xep.ChatMarkers.MARKER_DISPLAYED && marker != Xep.ChatMarkers.MARKER_ACKNOWLEDGED) return; Conversation? conversation = stream_interactor.get_module(MessageStorage.IDENTITY).get_conversation_for_stanza_id(account, stanza_id); if (conversation == null) return; - Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation); + Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation); if (message == null) return; conversation.read_up_to = message; } else { foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_conversations(jid, account)) { - Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation); + Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation); if (message != null) { switch (marker) { case Xep.ChatMarkers.MARKER_RECEIVED: diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index d02e4c71..01cc2f52 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 7; + private const int VERSION = 8; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -34,6 +34,20 @@ public class Database : Qlite.Database { } } + public class ContentTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column conversation_id = new Column.Integer("conversation_id") { not_null = true }; + public Column time = new Column.Long("time") { not_null = true }; + public Column local_time = new Column.Long("local_time") { not_null = true }; + public Column content_type = new Column.Integer("content_type") { not_null = true }; + public Column foreign_id = new Column.Integer("foreign_id") { not_null = true }; + + internal ContentTable(Database db) { + base(db, "content"); + init({id, conversation_id, time, local_time, content_type, foreign_id}); + } + } + public class MessageTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column stanza_id = new Column.Text("stanza_id"); @@ -174,6 +188,7 @@ public class Database : Qlite.Database { public AccountTable account { get; private set; } public JidTable jid { get; private set; } + public ContentTable content { get; private set; } public MessageTable message { get; private set; } public RealJidTable real_jid { get; private set; } public FileTransferTable file_transfer { get; private set; } @@ -191,6 +206,7 @@ public class Database : Qlite.Database { base(fileName, VERSION); account = new AccountTable(this); jid = new JidTable(this); + content = new ContentTable(this); message = new MessageTable(this); real_jid = new RealJidTable(this); file_transfer = new FileTransferTable(this); @@ -199,7 +215,7 @@ public class Database : Qlite.Database { entity_feature = new EntityFeatureTable(this); roster = new RosterTable(this); settings = new SettingsTable(this); - init({ account, jid, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings }); + init({ account, jid, content, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings }); try { exec("PRAGMA synchronous=0"); } catch (Error e) { } @@ -209,6 +225,24 @@ public class Database : Qlite.Database { // new table columns are added, outdated columns are still present if (oldVersion < 7) { message.fts_rebuild(); + } else if (oldVersion < 8) { + exec(""" + insert into content (conversation_id, time, local_time, content_type, foreign_id) + select conversation.id, message.time, message.local_time, 1, message.id + from message join conversation on + message.account_id=conversation.account_id and + message.counterpart_id=conversation.jid_id and + message.type=conversation.type+1 and + (message.counterpart_resource=conversation.resource or message.type != 3) + where + message.body not in (select info from file_transfer where info not null) and + message.id not in (select info from file_transfer where info not null) + union + select conversation.id, file_transfer.time, file_transfer.local_time, 2, file_transfer.id + from file_transfer join conversation on + file_transfer.account_id=conversation.account_id and + file_transfer.counterpart_id=conversation.jid_id + order by message.local_time, message.time"""); } } @@ -236,6 +270,16 @@ public class Database : Qlite.Database { } } + public int add_content_item(Conversation conversation, DateTime time, DateTime local_time, int content_type, int foreign_id) { + return (int) content.insert() + .value(content.conversation_id, conversation.id) + .value(content.local_time, (long) local_time.to_unix()) + .value(content.time, (long) time.to_unix()) + .value(content.content_type, content_type) + .value(content.foreign_id, foreign_id) + .perform(); + } + public Gee.List get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) { QueryBuilder select = message.select() .with(message.counterpart_id, "=", get_jid_id(jid)) diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index abc8acb4..9fb6ab19 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -76,7 +76,15 @@ public class MessageStorage : StreamInteractionModule, Object { return db_messages; } - public Message? get_message_by_id(string stanza_id, Conversation conversation) { + public Message? get_message_by_id(int id, Conversation conversation) { + init_conversation(conversation); + foreach (Message message in messages[conversation]) { + if (message.id == id) return message; + } + return null; + } + + public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) { init_conversation(conversation); foreach (Message message in messages[conversation]) { if (message.stanza_id == stanza_id) return message; diff --git a/main/src/ui/conversation_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala index cec54c7b..9ebb9159 100644 --- a/main/src/ui/conversation_summary/content_populator.vala +++ b/main/src/ui/conversation_summary/content_populator.vala @@ -19,13 +19,14 @@ public class ContentProvider : ContentItemCollection, Object { } 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(ContentItemAccumulator.IDENTITY).init(conversation, this); + stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this); } - public void close(Conversation conversation) { } - public void insert_item(ContentItem item) { item_collection.insert_item(new ContentMetaItem(item, widget_factory)); } @@ -34,7 +35,7 @@ public class ContentProvider : ContentItemCollection, Object { public Gee.List populate_latest(Conversation conversation, int n) { - Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_latest(this, conversation, n); + Gee.List items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation, n); Gee.List ret = new ArrayList(); foreach (ContentItem item in items) { ret.add(new ContentMetaItem(item, widget_factory)); @@ -42,29 +43,27 @@ public class ContentProvider : ContentItemCollection, Object { return ret; } - public Gee.List populate_before(Conversation conversation, Plugins.MetaConversationItem before_item, int n) { + public Gee.List populate_before(Conversation conversation, ContentItem before_item, int n) { Gee.List ret = new ArrayList(); - ContentMetaItem? content_meta_item = before_item as ContentMetaItem; - if (content_meta_item != null) { - Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_before(this, conversation, content_meta_item.content_item, n); - foreach (ContentItem item in items) { - ret.add(new ContentMetaItem(item, widget_factory)); - } + Gee.List 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 populate_after(Conversation conversation, Plugins.MetaConversationItem before_item, int n) { + public Gee.List populate_after(Conversation conversation, ContentItem after_item, int n) { Gee.List ret = new ArrayList(); - ContentMetaItem? content_meta_item = before_item as ContentMetaItem; - if (content_meta_item != null) { - Gee.List items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_after(this, conversation, content_meta_item.content_item, n); - foreach (ContentItem item in items) { - ret.add(new ContentMetaItem(item, widget_factory)); - } + Gee.List 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 { diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 870b6ee3..a1863cf4 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -270,9 +270,13 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { was_value = scrolled.vadjustment.value; if (!reloading_mutex.trylock()) return; if (meta_items.size > 0) { - Gee.List items = content_populator.populate_before(conversation, content_items.first(), 20); + Gee.List items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); foreach (ContentMetaItem item in items) { do_insert_item(item); + if (content_items.size > 50) { + do_remove_item(content_items.last()); + at_current_content = false; + } } } else { reloading_mutex.unlock(); @@ -286,7 +290,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { ContentMetaItem b = a as ContentMetaItem; MessageItem c = b.content_item as MessageItem; } - Gee.List items = content_populator.populate_after(conversation, content_items.last(), 20); + Gee.List items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); ContentMetaItem b = content_items.last() as ContentMetaItem; MessageItem c = b.content_item as MessageItem; diff --git a/plugins/http-files/src/plugin.vala b/plugins/http-files/src/plugin.vala index c57ee3dc..bd136f31 100644 --- a/plugins/http-files/src/plugin.vala +++ b/plugins/http-files/src/plugin.vala @@ -19,7 +19,7 @@ public class Plugin : RootInterface, Object { }); app.stream_interactor.get_module(FileManager.IDENTITY).add_provider(file_provider); - app.stream_interactor.get_module(ContentItemAccumulator.IDENTITY).add_filter(new FileMessageFilter(app.db)); + app.stream_interactor.get_module(ContentItemStore.IDENTITY).add_filter(new FileMessageFilter(app.db)); } public void shutdown() { From 4901b096708ff5ca54c3e5393de74f2a8be55894 Mon Sep 17 00:00:00 2001 From: bobufa Date: Sun, 22 Jul 2018 16:58:51 +0200 Subject: [PATCH 10/21] add search filter expressions --- libdino/src/service/search_processor.vala | 89 ++++++++++++++++++----- main/src/ui/unified_window.vala | 11 +++ plugins/http-files/src/manager.vala | 4 +- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala index 3c1057ae..6962a7c1 100644 --- a/libdino/src/service/search_processor.vala +++ b/libdino/src/service/search_processor.vala @@ -23,31 +23,82 @@ public class SearchProcessor : StreamInteractionModule, Object { 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); + private QueryBuilder prepare_search(string query) { + string words = ""; + string? with = null; + string? in_ = null; + string? from = null; + foreach(string word in query.split(" ")) { + if (word.has_prefix("with:")) { + if (with == null) { + with = word.substring(5) + "%"; + } else { + return db.message.select().where("0"); + } + } else if (word.has_prefix("in:")) { + if (in_ == null) { + in_ = word.substring(3) + "%"; + } else { + return db.message.select().where("0"); + } + } else if (word.has_prefix("from:")) { + if (from == null) { + from = word.substring(5) + "%"; + } else { + return db.message.select().where("0"); + } + } else { + words += word + "* "; + } } - foreach (Row row in query) { + if (in_ != null && with != null) { + return db.message.select().where("0"); + } + + QueryBuilder rows = db.message + .match(db.message.body, words) + .order_by(db.message.id, "DESC") + .join_with(db.jid, db.jid.id, db.message.counterpart_id) + .join_with(db.account, db.account.id, db.message.account_id) + .outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id); + if (with != null) { + if (with.index_of("/") > 0) { + rows.with(db.message.type_, "=", Message.Type.GROUPCHAT_PM) + .with(db.jid.bare_jid, "LIKE", with.substring(0, with.index_of("/"))) + .with(db.message.counterpart_resource, "LIKE", with.substring(with.index_of("/") + 1)); + } else { + rows.where(@"($(db.message.type_) = $((int)Message.Type.CHAT) AND $(db.jid.bare_jid) LIKE ?)" + + @" OR ($(db.message.type_) = $((int)Message.Type.GROUPCHAT_PM) AND $(db.real_jid.real_jid) LIKE ?)" + + @" OR ($(db.message.type_) = $((int)Message.Type.GROUPCHAT_PM) AND $(db.message.counterpart_resource) LIKE ?)", {with, with, with}); + } + } else if (in_ != null) { + rows.with(db.jid.bare_jid, "LIKE", in_) + .with(db.message.type_, "=", Message.Type.GROUPCHAT); + } + if (from != null) { + rows.where(@"($(db.message.direction) = 1 AND $(db.account.bare_jid) LIKE ?)" + + @" OR ($(db.message.direction) = 1 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.message.our_resource) LIKE ?)" + + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) = $((int)Message.Type.CHAT) AND $(db.jid.bare_jid) LIKE ?)" + + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.real_jid.real_jid) LIKE ?)" + + @" OR ($(db.message.direction) = 0 AND $(db.message.type_) IN ($((int)Message.Type.GROUPCHAT), $((int)Message.Type.GROUPCHAT_PM)) AND $(db.message.counterpart_resource) LIKE ?)", {from, from, from, from, from}); + } + return rows; + } + + public Gee.List match_messages(string query, int offset = -1) { + Gee.List ret = new ArrayList(Message.equals_func); + var rows = prepare_search(query).limit(10); + if (offset > 0) { + rows.offset(offset); + } + foreach (Row row in rows) { 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; + public int count_match_messages(string query) { + return (int)prepare_search(query).select({db.message.id}).count(); } } diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index e5444f9d..cfcd2bff 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -42,6 +42,17 @@ 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) { + if (conversation_frame.conversation != null) { + switch (conversation_frame.conversation.type_) { + case Conversation.Type.CHAT: + case Conversation.Type.GROUPCHAT_PM: + search_box.search_entry.text = @"with:$(conversation_frame.conversation.counterpart) "; + break; + case Conversation.Type.GROUPCHAT: + search_box.search_entry.text = @"in:$(conversation_frame.conversation.counterpart) "; + break; + } + } search_box.search_entry.grab_focus(); } else { search_box.search_entry.text = ""; diff --git a/plugins/http-files/src/manager.vala b/plugins/http-files/src/manager.vala index 78c244e3..9faa8933 100644 --- a/plugins/http-files/src/manager.vala +++ b/plugins/http-files/src/manager.vala @@ -98,8 +98,8 @@ public class FileMessageFilter : ContentFilter, Object { } private bool message_is_file(Database db, Entities.Message message) { - Qlite.QueryBuilder builder = db.file_transfer.select().with(db.file_transfer.info, "=", message.id.to_string()); - Qlite.QueryBuilder builder2 = db.file_transfer.select().with(db.file_transfer.info, "=", message.body); + Qlite.QueryBuilder builder = db.file_transfer.select({db.file_transfer.id}).with(db.file_transfer.info, "=", message.id.to_string()); + Qlite.QueryBuilder builder2 = db.file_transfer.select({db.file_transfer.id}).with(db.file_transfer.info, "=", message.body); return builder.count() > 0 || builder2.count() > 0; } From e376a577b6bfcdd9bdc0cc6ca283d99199a0197a Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 25 Jul 2018 20:41:51 +0200 Subject: [PATCH 11/21] improve sidebar UI - only display messages that are content items - only display messages for active accounts - "fix" textview issue - add empty states (no search, no results) --- libdino/src/service/database.vala | 5 +- libdino/src/service/message_storage.vala | 18 ++- libdino/src/service/search_processor.vala | 21 ++- main/data/global_search.ui | 137 ++++++++++++++++-- main/data/theme.css | 16 ++ main/src/ui/application.vala | 2 +- .../conversation_view.vala | 93 ++++++++---- .../message_textview.vala | 1 - .../conversation_titlebar/search_entry.vala | 4 +- main/src/ui/global_search.vala | 129 ++++++++++------- main/src/ui/unified_window.vala | 23 ++- main/src/ui/util/helper.vala | 13 +- 12 files changed, 341 insertions(+), 121 deletions(-) diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 01cc2f52..bea07dda 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -43,8 +43,9 @@ public class Database : Qlite.Database { public Column foreign_id = new Column.Integer("foreign_id") { not_null = true }; internal ContentTable(Database db) { - base(db, "content"); + base(db, "contentx"); init({id, conversation_id, time, local_time, content_type, foreign_id}); + unique({content_type, foreign_id}, "IGNORE"); } } @@ -227,7 +228,7 @@ public class Database : Qlite.Database { message.fts_rebuild(); } else if (oldVersion < 8) { exec(""" - insert into content (conversation_id, time, local_time, content_type, foreign_id) + insert into contentx (conversation_id, time, local_time, content_type, foreign_id) select conversation.id, message.time, message.local_time, 1, message.id from message join conversation on message.account_id=conversation.account_id and diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index 9fb6ab19..50fc94b3 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -52,7 +52,7 @@ public class MessageStorage : StreamInteractionModule, Object { return null; } - public Gee.List? get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) { + public Gee.List get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) { // SortedSet? before = messages[conversation].head_set(message); // if (before != null && before.size >= count) { // Gee.List ret = new ArrayList(Message.equals_func); @@ -66,14 +66,22 @@ public class MessageStorage : StreamInteractionModule, Object { // } // return ret; // } else { - Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id); - return db_messages; + Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id); + Gee.List ret = new ArrayList(); + foreach (Message message in db_messages) { + ret.add(new MessageItem(message, conversation, -1)); + } + return ret; // } } - public Gee.List? get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) { + public Gee.List get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) { Gee.List db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id); - return db_messages; + Gee.List ret = new ArrayList(); + foreach (Message message in db_messages) { + ret.add(new MessageItem(message, conversation, -1)); + } + return ret; } public Message? get_message_by_id(int id, Conversation conversation) { diff --git a/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala index 6962a7c1..e56efa41 100644 --- a/libdino/src/service/search_processor.vala +++ b/libdino/src/service/search_processor.vala @@ -23,7 +23,7 @@ public class SearchProcessor : StreamInteractionModule, Object { this.db = db; } - private QueryBuilder prepare_search(string query) { + private QueryBuilder prepare_search(string query, bool join_content) { string words = ""; string? with = null; string? in_ = null; @@ -60,7 +60,12 @@ public class SearchProcessor : StreamInteractionModule, Object { .order_by(db.message.id, "DESC") .join_with(db.jid, db.jid.id, db.message.counterpart_id) .join_with(db.account, db.account.id, db.message.account_id) - .outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id); + .outer_join_with(db.real_jid, db.real_jid.message_id, db.message.id) + .with(db.account.enabled, "=", true); + if (join_content) { + rows.join_on(db.content, "message.id=contentx.foreign_id AND contentx.content_type=1") + .with(db.content.content_type, "=", 1); + } if (with != null) { if (with.index_of("/") > 0) { rows.with(db.message.type_, "=", Message.Type.GROUPCHAT_PM) @@ -85,20 +90,22 @@ public class SearchProcessor : StreamInteractionModule, Object { return rows; } - public Gee.List match_messages(string query, int offset = -1) { - Gee.List ret = new ArrayList(Message.equals_func); - var rows = prepare_search(query).limit(10); + public Gee.List match_messages(string query, int offset = -1) { + Gee.List ret = new ArrayList(); + var rows = prepare_search(query, true).limit(10); if (offset > 0) { rows.offset(offset); } foreach (Row row in rows) { - ret.add(new Message.from_row(db, row)); + Message message = new Message.from_row(db, row); + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message); + ret.add(new MessageItem(message, conversation, row[db.content.id])); } return ret; } public int count_match_messages(string query) { - return (int)prepare_search(query).select({db.message.id}).count(); + return (int)prepare_search(query, false).select({db.message.id}).count(); } } diff --git a/main/data/global_search.ui b/main/data/global_search.ui index cc5f043b..3c4597c1 100644 --- a/main/data/global_search.ui +++ b/main/data/global_search.ui @@ -10,24 +10,135 @@
- - 0 - True - 17 - True - - - - - True + True - + vertical - 25 - 10 + 10 + center True + + + True + system-search-symbolic + 4 + 72 + + + + + + No active search + 0.5 + 0.5 + True + + + + + + + + + + Type to start a search + 0.5 + 0.5 + True + + + + + empty + + + + + vertical + 10 + center + True + + + True + face-uncertain-symbolic + 4 + 72 + + + + + + No matching messages + 0.5 + 0.5 + True + + + + + + + + + + Check the spelling or try to remove filters + 0.5 + 0.5 + True + + + + + + no-result + + z + + + vertical + True + + + 0 + True + 17 + True + + + + + never + True + True + + + vertical + 25 + 10 + True + + + + + + + results + diff --git a/main/data/theme.css b/main/data/theme.css index 61f15af4..42988c42 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -17,6 +17,22 @@ window.dino-main .dino-conversation undershoot { background: none; } +@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; 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/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index a1863cf4..c74884a4 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -34,6 +34,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private bool animate = false; private bool firstLoad = true; private bool at_current_content = true; + private bool reload_messages = true; public ConversationView init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; @@ -57,7 +58,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { return true; }); - Util.force_base_background(this); return this; } @@ -66,14 +66,71 @@ 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 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 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; + }); + print(@"height_for_w: $(h)\n"); } + 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; + }); + print(@"timeout: $(h)\n"); + 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) { @@ -84,7 +141,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } } this.conversation = conversation; - stack.set_visible_child_name("void"); foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.init(conversation, this, Plugins.WidgetType.GTK); @@ -92,17 +148,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { content_populator.init(this, conversation, Plugins.WidgetType.GTK); subscription_notification.init(conversation, this); - display_latest(); - - stack.set_visible_child_name("main"); + animate = false; + Timeout.add(20, () => { animate = true; return false; }); } private void display_latest() { clear(); - was_upper = null; - was_page_size = null; - animate = false; - Timeout.add(20, () => { animate = true; return false; }); Gee.List items = content_populator.populate_latest(conversation, 40); foreach (ContentMetaItem item in items) { @@ -163,7 +214,7 @@ 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; @@ -174,7 +225,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? @@ -206,7 +257,7 @@ 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 @@ -222,6 +273,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } } } + return insert; } private void split_at_time(ConversationItemSkeleton split_skeleton, DateTime time) { @@ -273,10 +325,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { Gee.List items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); foreach (ContentMetaItem item in items) { do_insert_item(item); - if (content_items.size > 50) { - do_remove_item(content_items.last()); - at_current_content = false; - } } } else { reloading_mutex.unlock(); @@ -310,21 +358,14 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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; + 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(); item_skeletons.clear(); diff --git a/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala index 0c38c269..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); } diff --git a/main/src/ui/conversation_titlebar/search_entry.vala b/main/src/ui/conversation_titlebar/search_entry.vala index e80e5954..b452bdce 100644 --- a/main/src/ui/conversation_titlebar/search_entry.vala +++ b/main/src/ui/conversation_titlebar/search_entry.vala @@ -24,9 +24,7 @@ public class SearchMenuEntry : Plugins.ConversationTitlebarEntry, Object { } public class GlobalSearchButton : Plugins.ConversationTitlebarWidget, Gtk.ToggleButton { - public new void set_conversation(Conversation conversation) { - active = false; - } + public new void set_conversation(Conversation conversation) { } } } diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala index cadee9c1..8bd13e6f 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -1,3 +1,4 @@ +using Gee; using Gtk; using Pango; @@ -7,6 +8,8 @@ namespace Dino.Ui { [GtkTemplate (ui = "/im/dino/Dino/global_search.ui")] class GlobalSearch : Box { + public signal void selected_item(MessageItem item); + private StreamInteractor stream_interactor; private string search = ""; private int loaded_results = -1; @@ -16,6 +19,7 @@ class GlobalSearch : Box { [GtkChild] public Label entry_number_label; [GtkChild] public ScrolledWindow results_scrolled; [GtkChild] public Box results_box; + [GtkChild] public Stack results_empty_stack; public GlobalSearch init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; @@ -27,7 +31,7 @@ class GlobalSearch : Box { 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); + Gee.List new_messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search, loaded_results); if (new_messages.size == 0) { reloading_mutex.unlock(); return; @@ -51,37 +55,47 @@ class GlobalSearch : Box { 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); + if (get_keywords(search).is_empty) { + results_empty_stack.set_visible_child_name("empty"); + return; + } + + Gee.List 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 search results").printf(match_count) + ""; + 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); + private void append_messages(Gee.List messages) { + foreach (MessageItem item in messages) { + Gee.List before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(item.conversation, item.message.local_time, item.message.id, 1); + Gee.List 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())); } - context_box.add(get_match_message_widget(message)); + + 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(message.time)) { xalign=0, visible=true }; + 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, conversation); - string title = message.type_ == Message.Type.GROUPCHAT ? _("In %s").printf(display_name) : _("With %s").printf(display_name); + 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(@"$(Markup.escape_text(title))") { ellipsize=EllipsizeMode.END, xalign=0, use_markup=true, visible=true }); header_box.add(date_label); @@ -94,21 +108,12 @@ class GlobalSearch : 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); + private Widget get_match_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); grid.margin_top = 3; grid.margin_bottom = 3; - string text = message.body.replace("\n", "").replace("\r", ""); + string text = item.message.body.replace("\n", "").replace("\r", ""); if (text.length > 200) { int index = text.index_of(search); if (index + search.length <= 100) { @@ -123,52 +128,68 @@ class GlobalSearch : Box { 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); + Gee.List 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); - // force_alloc_width(tv, this.width_request); - 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(Message message) { - Grid grid = get_skeleton(message); + private Grid get_context_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); grid.margin_left = 7; - Label label = new Label(message.body.replace("\n", "").replace("\r", "")) { ellipsize=EllipsizeMode.MIDDLE, xalign=0, visible=true }; + 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(Message message) { + 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, message.from, message.account); + 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, 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) + 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 = @"$display_name"; grid.attach(name_label, 1, 0, 1, 1); return grid; } + + private static Gee.List get_keywords(string search_string) { + Gee.List ret = new ArrayList(); + 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 cfcd2bff..60aeb832 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -39,7 +39,10 @@ public class UnifiedWindow : Window { setup_unified(); setup_stack(); - conversation_titlebar.search_button.bind_property("active", search_revealer, "reveal-child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + + 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) { @@ -58,6 +61,10 @@ public class UnifiedWindow : Window { search_box.search_entry.text = ""; } }); + search_box.selected_item.connect((item) => { + on_conversation_selected(item.conversation, false, false); + conversation_frame.initialize_around_message(item.conversation, item); + }); paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); @@ -71,8 +78,8 @@ 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(); } @@ -89,15 +96,21 @@ public class UnifiedWindow : Window { search_revealer.valign = Align.FILL; } - public void on_conversation_selected(Conversation conversation) { + public void on_conversation_selected(Conversation conversation, bool close_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 (close_search) { + conversation_titlebar.search_button.active = false; + search_revealer.reveal_child = false; + } 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); } } 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); +} + } From a6457286240700a83dbf4e3404e2a1180c95c8c0 Mon Sep 17 00:00:00 2001 From: bobufa Date: Sun, 29 Jul 2018 23:24:53 +0200 Subject: [PATCH 12/21] hide search bar when clicking outside of it, only reset search entry on conversation change --- main/data/theme.css | 1 + .../conversation_view.vala | 10 --- main/src/ui/unified_window.vala | 65 +++++++++++-------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/main/data/theme.css b/main/data/theme.css index 42988c42..ce195924 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -36,6 +36,7 @@ window.dino-main .dino-conversation textview, window.dino-main .dino-conversatio 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 { diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index c74884a4..07259cc5 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -112,7 +112,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { widget.get_preferred_height_for_width(main.get_allocated_width() - 2 * main.margin, out minimum_height, out natural_height); h += minimum_height + 15; }); - print(@"height_for_w: $(h)\n"); } reload_messages = false; @@ -124,7 +123,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { i += sk != null ? sk.items.size : 1; h += widget.get_allocated_height() + 15; }); - print(@"timeout: $(h)\n"); scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; w.get_style_context().add_class("highlight-once"); reload_messages = true; @@ -334,15 +332,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private void load_later_messages() { if (!reloading_mutex.trylock()) return; if (meta_items.size > 0 && !at_current_content) { - foreach (Plugins.MetaConversationItem a in content_items) { - ContentMetaItem b = a as ContentMetaItem; - MessageItem c = b.content_item as MessageItem; - } Gee.List items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); - - ContentMetaItem b = content_items.last() as ContentMetaItem; - MessageItem c = b.content_item as MessageItem; - if (items.size == 0) { at_current_content = true; } diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 60aeb832..115e392c 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 }; @@ -45,26 +46,30 @@ public class UnifiedWindow : Window { }); search_revealer.notify["child-revealed"].connect(() => { if (search_revealer.child_revealed) { - if (conversation_frame.conversation != null) { - switch (conversation_frame.conversation.type_) { - case Conversation.Type.CHAT: - case Conversation.Type.GROUPCHAT_PM: - search_box.search_entry.text = @"with:$(conversation_frame.conversation.counterpart) "; - break; - case Conversation.Type.GROUPCHAT: - search_box.search_entry.text = @"in:$(conversation_frame.conversation.counterpart) "; - break; - } + if (conversation_frame.conversation != null && search_box.search_entry.text == "") { + reset_search_entry(); } search_box.search_entry.grab_focus(); - } else { - search_box.search_entry.text = ""; } }); search_box.selected_item.connect((item) => { on_conversation_selected(item.conversation, false, false); conversation_frame.initialize_around_message(item.conversation, item); }); + 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); + if (ret && event.button.x_root < dest_x) { + 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); @@ -84,28 +89,29 @@ 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 reset_search_entry() { + if (conversation_frame.conversation != null) { + switch (conversation_frame.conversation.type_) { + case Conversation.Type.CHAT: + case Conversation.Type.GROUPCHAT_PM: + search_box.search_entry.text = @"with:$(conversation_frame.conversation.counterpart) "; + break; + case Conversation.Type.GROUPCHAT: + search_box.search_entry.text = @"in:$(conversation_frame.conversation.counterpart) "; + break; + } + } } - 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, bool close_search = true, bool default_initialize_conversation = true) { + 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 (close_search) { - conversation_titlebar.search_button.active = false; - search_revealer.reveal_child = false; + if (do_reset_search) { + reset_search_entry(); } chat_input.initialize_for_conversation(conversation); if (default_initialize_conversation) { @@ -115,6 +121,11 @@ public class UnifiedWindow : Window { } } + private void close_search() { + conversation_titlebar.search_button.active = false; + search_revealer.reveal_child = false; + } + private void setup_unified() { Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui"); paned = (Paned) builder.get_object("paned"); From ec25ecf2024fdd6b0dd70adef9ef644fad3f77f9 Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 31 Jul 2018 10:55:15 +0200 Subject: [PATCH 13/21] filter out messages outside the range of displayed messages --- .../conversation_view.vala | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 07259cc5..f9f0bef0 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -44,7 +44,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { content_populator = new ContentProvider(stream_interactor); subscription_notification = new SubscriptionNotitication(stream_interactor); - insert_item.connect(do_insert_item); + insert_item.connect(filter_insert_item); remove_item.connect(do_remove_item); Application app = GLib.Application.get_default() as Application; @@ -160,6 +160,16 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { Idle.add(() => { on_value_notify(); return false; }); } + public void filter_insert_item(Plugins.MetaConversationItem item) { + print(@"$(meta_items.last().sort_time.compare(item.sort_time))\n"); + print(@"$(meta_items.first().sort_time.compare(item.sort_time))\n"); + if (at_current_content && meta_items.last().sort_time.compare(item.sort_time) < 0) { + do_insert_item(item); + } else if (meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0) { + do_insert_item(item); + } + } + public void do_insert_item(Plugins.MetaConversationItem item) { lock (meta_items) { if (!item.can_merge || !merge_back(item)) { @@ -174,17 +184,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { private void do_remove_item(Plugins.MetaConversationItem item) { ConversationItemSkeleton? skeleton = item_item_skeletons[item]; - if (skeleton.items.size > 1) { - skeleton.remove_meta_item(item); - } else { - widgets[item].destroy(); - widgets.unset(item); - skeleton.destroy(); - item_skeletons.remove(skeleton); - item_item_skeletons.unset(item); + if (skeleton != null) { + if (skeleton.items.size > 1) { + skeleton.remove_meta_item(item); + } else { + widgets[item].destroy(); + widgets.unset(item); + skeleton.destroy(); + item_skeletons.remove(skeleton); + item_item_skeletons.unset(item); + } + content_items.remove(item); + meta_items.remove(item); } - content_items.remove(item); - meta_items.remove(item); } public void add_notification(Widget widget) { From d025387ab95719ebfd7e41676c6f4138bc0afc44 Mon Sep 17 00:00:00 2001 From: bobufa Date: Fri, 3 Aug 2018 19:41:23 +0200 Subject: [PATCH 14/21] fix message filter in conversation view for first message --- .../conversation_view.vala | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index f9f0bef0..7fce6673 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -161,13 +161,15 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { } public void filter_insert_item(Plugins.MetaConversationItem item) { - print(@"$(meta_items.last().sort_time.compare(item.sort_time))\n"); - print(@"$(meta_items.first().sort_time.compare(item.sort_time))\n"); - if (at_current_content && meta_items.last().sort_time.compare(item.sort_time) < 0) { - do_insert_item(item); - } else if (meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0) { - do_insert_item(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 do_insert_item(Plugins.MetaConversationItem item) { @@ -359,8 +361,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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; } From b35abad05455970da36aafafd4afc9148aa43ced Mon Sep 17 00:00:00 2001 From: bobufa Date: Sat, 4 Aug 2018 12:48:00 +0200 Subject: [PATCH 15/21] fix wrong conversation being prefilled --- main/src/ui/unified_window.vala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 115e392c..22b8f49a 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -91,13 +91,13 @@ public class UnifiedWindow : Gtk.Window { private void reset_search_entry() { if (conversation_frame.conversation != null) { - switch (conversation_frame.conversation.type_) { + switch (conversation.type_) { case Conversation.Type.CHAT: case Conversation.Type.GROUPCHAT_PM: - search_box.search_entry.text = @"with:$(conversation_frame.conversation.counterpart) "; + search_box.search_entry.text = @"with:$(conversation.counterpart) "; break; case Conversation.Type.GROUPCHAT: - search_box.search_entry.text = @"in:$(conversation_frame.conversation.counterpart) "; + search_box.search_entry.text = @"in:$(conversation.counterpart) "; break; } } From dfc7d2a890d755bcaf526e586e14046b581ed094 Mon Sep 17 00:00:00 2001 From: bobufa Date: Tue, 7 Aug 2018 16:03:00 +0200 Subject: [PATCH 16/21] add button that scrolls to the bottom of the conversation --- main/data/unified_main_content.ui | 49 ++++++++++++++++--- .../conversation_view.vala | 5 +- main/src/ui/unified_window.vala | 13 +++++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui index 61781ac4..d5897b1a 100644 --- a/main/data/unified_main_content.ui +++ b/main/data/unified_main_content.ui @@ -17,20 +17,53 @@ True - - vertical + True - - + + vertical True + + + + True + + + + + True + + - - + + + end + end + crossfade True + + + False + end + end + 70 + 100 + True + + + + go-down-symbolic + 1 + True + + + + diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 7fce6673..e6a564de 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -11,7 +11,7 @@ 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; @@ -307,8 +307,7 @@ 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 + 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 } diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 22b8f49a..97ef7ed4 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -18,6 +18,8 @@ public class UnifiedWindow : Gtk.Window { 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; + private Revealer goto_end_revealer; + private Button goto_end_button; private Revealer search_revealer; private SearchEntry search_entry; private GlobalSearch search_box; @@ -40,6 +42,13 @@ public class UnifiedWindow : Gtk.Window { setup_unified(); setup_stack(); + 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; @@ -55,11 +64,13 @@ public class UnifiedWindow : Gtk.Window { 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); + print(@"ret $(ret) button-x $(event.button.x_root) !< dest_x $(dest_x)\n"); if (ret && event.button.x_root < dest_x) { close_search(); } @@ -132,6 +143,8 @@ public class UnifiedWindow : Gtk.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); + 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"); From b0c94641fcf85ed55976e894fea970331ae22892 Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 8 Aug 2018 11:35:41 +0200 Subject: [PATCH 17/21] fix closing sidebar on click outside --- main/src/ui/unified_window.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 97ef7ed4..61a22085 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -70,8 +70,9 @@ public class UnifiedWindow : Gtk.Window { 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); - print(@"ret $(ret) button-x $(event.button.x_root) !< dest_x $(dest_x)\n"); - if (ret && event.button.x_root < dest_x) { + 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) { From c0844bdea428c10949339960bd16ea5e2a335fb8 Mon Sep 17 00:00:00 2001 From: bobufa Date: Wed, 1 Aug 2018 15:20:56 +0200 Subject: [PATCH 18/21] add suggestions/auto-complete for search filters --- libdino/src/service/search_processor.vala | 159 ++++++++++++- main/CMakeLists.txt | 1 + main/data/global_search.ui | 265 ++++++++++++---------- 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, 418 insertions(+), 138 deletions(-) create mode 100644 main/data/search_autocomplete.ui 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 @@ -