aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfiaxh <fiaxh@users.noreply.github.com>2018-08-31 16:25:51 +0200
committerGitHub <noreply@github.com>2018-08-31 16:25:51 +0200
commitecb18afdb51b3230ea451a27a5b345cf5100f02e (patch)
tree44bae666c4cc31afce3a5d6891f6b4f65dfcebb1
parent9e93a77a624aed24402cf1ece69c05aaa0aab600 (diff)
parentf5547076d2397cec5c9d7374bd161f93327488c4 (diff)
downloaddino-ecb18afdb51b3230ea451a27a5b345cf5100f02e.tar.gz
dino-ecb18afdb51b3230ea451a27a5b345cf5100f02e.zip
Merge pull request #415 from bobufa/message-search
Message search
-rw-r--r--libdino/CMakeLists.txt2
-rw-r--r--libdino/src/application.vala2
-rw-r--r--libdino/src/entity/file_transfer.vala22
-rw-r--r--libdino/src/entity/message.vala2
-rw-r--r--libdino/src/plugin/interfaces.vala26
-rw-r--r--libdino/src/plugin/registry.vala21
-rw-r--r--libdino/src/service/content_item_store.vala245
-rw-r--r--libdino/src/service/counterpart_interaction_manager.vala6
-rw-r--r--libdino/src/service/database.vala88
-rw-r--r--libdino/src/service/file_manager.vala43
-rw-r--r--libdino/src/service/message_storage.vala58
-rw-r--r--libdino/src/service/search_processor.vala263
-rw-r--r--main/CMakeLists.txt13
-rw-r--r--main/data/conversation_list_titlebar.ui15
-rw-r--r--main/data/conversation_selector/view.ui15
-rw-r--r--main/data/global_search.ui169
-rw-r--r--main/data/menu_add.ui1
-rw-r--r--main/data/menu_app.ui1
-rw-r--r--main/data/menu_conversation.ui1
-rw-r--r--main/data/search_autocomplete.ui24
-rw-r--r--main/data/theme.css41
-rw-r--r--main/data/unified_main_content.ui100
-rw-r--r--main/src/ui/application.vala2
-rw-r--r--main/src/ui/chat_input/view.vala3
-rw-r--r--main/src/ui/conversation_list_titlebar.vala1
-rw-r--r--main/src/ui/conversation_selector/view.vala33
-rw-r--r--main/src/ui/conversation_summary/chat_state_populator.vala4
-rw-r--r--main/src/ui/conversation_summary/content_item_widget_factory.vala224
-rw-r--r--main/src/ui/conversation_summary/content_populator.vala110
-rw-r--r--main/src/ui/conversation_summary/conversation_item_skeleton.vala2
-rw-r--r--main/src/ui/conversation_summary/conversation_view.vala207
-rw-r--r--main/src/ui/conversation_summary/date_separator_populator.vala4
-rw-r--r--main/src/ui/conversation_summary/default_file_display.vala95
-rw-r--r--main/src/ui/conversation_summary/default_message_display.vala58
-rw-r--r--main/src/ui/conversation_summary/file_populator.vala54
-rw-r--r--main/src/ui/conversation_summary/image_display.vala137
-rw-r--r--main/src/ui/conversation_summary/message_populator.vala81
-rw-r--r--main/src/ui/conversation_summary/message_textview.vala5
-rw-r--r--main/src/ui/conversation_summary/slashme_message_display.vala79
-rw-r--r--main/src/ui/conversation_titlebar/search_entry.vala30
-rw-r--r--main/src/ui/conversation_titlebar/view.vala3
-rw-r--r--main/src/ui/global_search.vala259
-rw-r--r--main/src/ui/unified_window.vala107
-rw-r--r--main/src/ui/util/helper.vala13
-rw-r--r--plugins/http-files/src/file_provider.vala2
-rw-r--r--plugins/http-files/src/manager.vala23
-rw-r--r--plugins/http-files/src/plugin.vala2
-rw-r--r--qlite/src/column.vala57
-rw-r--r--qlite/src/database.vala5
-rw-r--r--qlite/src/query_builder.vala92
-rw-r--r--qlite/src/row.vala61
-rw-r--r--qlite/src/table.vala57
52 files changed, 2145 insertions, 823 deletions
diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt
index 9b3e991a..f45d08f5 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_store.vala
src/service/conversation_manager.vala
src/service/counterpart_interaction_manager.vala
src/service/database.vala
@@ -42,6 +43,7 @@ SOURCES
src/service/presence_manager.vala
src/service/registration.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 370618b2..7f278fa0 100644
--- a/libdino/src/application.vala
+++ b/libdino/src/application.vala
@@ -38,6 +38,8 @@ public interface Dino.Application : GLib.Application {
ChatInteraction.start(stream_interactor);
FileManager.start(stream_interactor, db);
NotificationEvents.start(stream_interactor);
+ ContentItemStore.start(stream_interactor, db);
+ SearchProcessor.start(stream_interactor, db);
create_actions();
diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala
index e2542e74..be472796 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
@@ -61,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];
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<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
- internal Gee.List<MessageDisplayProvider> message_displays = new ArrayList<MessageDisplayProvider>();
- internal Gee.List<ConversationItemPopulator> conversation_item_populators = new ArrayList<ConversationItemPopulator>();
+ internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((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_store.vala b/libdino/src/service/content_item_store.vala
new file mode 100644
index 00000000..83ba1da0
--- /dev/null
+++ b/libdino/src/service/content_item_store.vala
@@ -0,0 +1,245 @@
+using Gee;
+
+using Dino.Entities;
+using Qlite;
+using Xmpp;
+
+namespace Dino {
+
+public class ContentItemStore : StreamInteractionModule, Object {
+ public static ModuleIdentity<ContentItemStore> IDENTITY = new ModuleIdentity<ContentItemStore>("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<ContentFilter> filters = new ArrayList<ContentFilter>();
+ private HashMap<Conversation, ContentItemCollection> collection_conversations = new HashMap<Conversation, ContentItemCollection>(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<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
+ Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(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<ContentItem> ret = new ArrayList<ContentItem>();
+ foreach (ContentItem item in items) {
+ ret.add(item);
+ }
+ return ret;
+ }
+
+ public Gee.List<ContentItem> 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<ContentItem> 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<ContentItem> 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, Conversation conversation) {
+ FileItem item = new FileItem(file_transfer, -1);
+ if (!discard(item)) {
+ 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 25db82f8..8a470d12 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 = 8;
public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@@ -34,6 +34,21 @@ public class Database : Qlite.Database {
}
}
+ public class ContentTable : Table {
+ public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+ public Column<int> conversation_id = new Column.Integer("conversation_id") { not_null = true };
+ public Column<long> time = new Column.Long("time") { not_null = true };
+ public Column<long> local_time = new Column.Long("local_time") { not_null = true };
+ public Column<int> content_type = new Column.Integer("content_type") { not_null = true };
+ public Column<int> foreign_id = new Column.Integer("foreign_id") { not_null = true };
+
+ internal ContentTable(Database db) {
+ base(db, "content_item");
+ init({id, conversation_id, time, local_time, content_type, foreign_id});
+ unique({content_type, foreign_id}, "IGNORE");
+ }
+ }
+
public class MessageTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<string> stanza_id = new Column.Text("stanza_id");
@@ -54,6 +69,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});
}
}
@@ -173,6 +189,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; }
@@ -190,6 +207,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);
@@ -198,7 +216,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) { }
@@ -206,6 +224,31 @@ 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();
+ } else if (oldVersion < 8) {
+ exec("""
+ insert into content_item (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, message.time, message.local_time, 2, file_transfer.id
+ from file_transfer
+ join message on
+ file_transfer.info=message.id
+ join conversation on
+ file_transfer.account_id=conversation.account_id and
+ file_transfer.counterpart_id=conversation.jid_id and
+ message.type=conversation.type+1 and
+ (message.counterpart_resource=conversation.resource or message.type != 3)""");
+ }
}
public ArrayList<Account> get_accounts() {
@@ -232,11 +275,41 @@ public class Database : Qlite.Database {
}
}
- public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before) {
- QueryBuilder select = message.select()
- .with(message.counterpart_id, "=", get_jid_id(jid))
+ 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<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) {
+ QueryBuilder select = message.select();
+
+ if (before != null) {
+ if (id > 0) {
+ select.where(@"local_time < ? OR (local_time = ? AND id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() });
+ } else {
+ select.with(message.id, "<", id);
+ }
+ }
+ if (after != null) {
+ if (id > 0) {
+ select.where(@"local_time > ? OR (local_time = ? AND id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() });
+ } else {
+ select.with(message.local_time, ">", (long) after.to_unix());
+ }
+ if (id > 0) {
+ select.with(message.id, ">", id);
+ }
+ } else {
+ select.order_by(message.id, "DESC");
+ }
+
+ 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);
@@ -244,9 +317,6 @@ public class Database : Qlite.Database {
if (type != null) {
select.with(message.type_, "=", (int) type);
}
- if (before != null) {
- select.with(message.local_time, "<", (long) before.to_unix());
- }
LinkedList<Message> ret = new LinkedList<Message>();
foreach (Row row in select) {
diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala
index 3def24af..340205af 100644
--- a/libdino/src/service/file_manager.vala
+++ b/libdino/src/service/file_manager.vala
@@ -11,7 +11,7 @@ public class FileManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } }
public signal void upload_available(Account account);
- public signal void received_file(FileTransfer file_transfer);
+ public signal void received_file(FileTransfer file_transfer, Conversation conversation);
private StreamInteractor stream_interactor;
private Database db;
@@ -68,7 +68,7 @@ public class FileManager : StreamInteractionModule, Object {
file_sender.send_file(conversation, file_transfer);
}
}
- received_file(file_transfer);
+ received_file(file_transfer, conversation);
}
public bool is_upload_available(Conversation conversation) {
@@ -78,21 +78,38 @@ public class FileManager : StreamInteractionModule, Object {
return false;
}
- public Gee.List<FileTransfer> get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) {
+ public Gee.List<FileTransfer> 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);
+ return get_transfers_from_qry(select);
+ }
+
+ public Gee.List<FileTransfer> 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)after.to_unix())
.with(db.file_transfer.local_time, "<", (long)before.to_unix())
- .order_by(db.file_transfer.id, "DESC");
+ .order_by(db.file_transfer.local_time, "DESC")
+ .limit(n);
+ return get_transfers_from_qry(select);
+ }
+
+ public Gee.List<FileTransfer> 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())
+ .limit(n);
+ return get_transfers_from_qry(select);
+ }
+ private Gee.List<FileTransfer> get_transfers_from_qry(Qlite.QueryBuilder select) {
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
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;
@@ -117,7 +134,7 @@ public class FileManager : StreamInteractionModule, Object {
outgoing_processors.add(processor);
}
- private void handle_incomming_file(FileTransfer file_transfer) {
+ private void handle_incomming_file(FileTransfer file_transfer, Conversation conversation) {
foreach (IncommingFileProcessor processor in incomming_processors) {
if (processor.can_process(file_transfer)) {
processor.process(file_transfer);
@@ -131,7 +148,7 @@ public class FileManager : StreamInteractionModule, Object {
} catch (Error e) { }
file_transfer.persist(db);
- received_file(file_transfer);
+ received_file(file_transfer, conversation);
}
private void save_file(FileTransfer file_transfer) {
@@ -152,7 +169,7 @@ public class FileManager : StreamInteractionModule, Object {
}
public interface FileProvider : Object {
- public signal void file_incoming(FileTransfer file_transfer);
+ public signal void file_incoming(FileTransfer file_transfer, Conversation conversation);
}
public interface FileSender : Object {
diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala
index 35e05074..50fc94b3 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;
@@ -51,26 +52,47 @@ public class MessageStorage : StreamInteractionModule, Object {
return null;
}
- public Gee.List<Message>? get_messages_before_message(Conversation? conversation, Message message, int count = 20) {
- SortedSet<Message>? before = messages[conversation].head_set(message);
- if (before != null && before.size >= count) {
- Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
- Iterator<Message> 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<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, message.local_time);
- return db_messages;
+ public Gee.List<MessageItem> get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) {
+// SortedSet<Message>? before = messages[conversation].head_set(message);
+// if (before != null && before.size >= count) {
+// Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
+// Iterator<Message> 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<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id);
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ foreach (Message message in db_messages) {
+ ret.add(new MessageItem(message, conversation, -1));
+ }
+ return ret;
+// }
+ }
+
+ public Gee.List<MessageItem> get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) {
+ Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id);
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ 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) {
+ init_conversation(conversation);
+ foreach (Message message in messages[conversation]) {
+ if (message.id == id) return message;
}
+ return null;
}
- public Message? get_message_by_id(string stanza_id, Conversation conversation) {
+ 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;
@@ -100,7 +122,7 @@ public class MessageStorage : StreamInteractionModule, Object {
}
return res;
});
- Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null);
+ Gee.List<Message> 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/libdino/src/service/search_processor.vala b/libdino/src/service/search_processor.vala
new file mode 100644
index 00000000..5a14dfe7
--- /dev/null
+++ b/libdino/src/service/search_processor.vala
@@ -0,0 +1,263 @@
+using Gee;
+
+using Xmpp;
+using Qlite;
+using Dino.Entities;
+
+namespace Dino {
+
+public class SearchProcessor : StreamInteractionModule, Object {
+ public static ModuleIdentity<SearchProcessor> IDENTITY = new ModuleIdentity<SearchProcessor>("search_processor");
+ public string id { get { return IDENTITY.id; } }
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ SearchProcessor m = new SearchProcessor(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ public SearchProcessor(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ }
+
+ private QueryBuilder prepare_search(string query, bool join_content) {
+ 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 + "* ";
+ }
+ }
+ 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)
+ .with(db.account.enabled, "=", true);
+ if (join_content) {
+ rows.join_on(db.content, "message.id=content_item.foreign_id AND content_item.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)
+ .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<SearchSuggestion> 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<SearchSuggestion> suggestions = new ArrayList<SearchSuggestion>();
+
+ 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<MessageItem> match_messages(string query, int offset = -1) {
+ Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
+ QueryBuilder rows = prepare_search(query, true).limit(10);
+ if (offset > 0) {
+ rows.offset(offset);
+ }
+ foreach (Row row in rows) {
+ 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, false).select({db.message.id}).count();
+ }
+}
+
+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 00ca692a..65d84bdd 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -29,6 +29,7 @@ set(RESOURCE_LIST
chat_input.ui
contact_details_dialog.ui
conversation_list_titlebar.ui
+ global_search.ui
conversation_selector/view.ui
conversation_selector/chat_row_tooltip.ui
conversation_selector/conversation_row.ui
@@ -43,7 +44,9 @@ set(RESOURCE_LIST
menu_encryption.ui
occupant_list.ui
occupant_list_item.ui
+ search_autocomplete.ui
settings_dialog.ui
+ unified_main_content.ui
unified_window_placeholder.ui
theme.css
@@ -93,6 +96,7 @@ SOURCES
src/ui/contact_details/dialog.vala
src/ui/contact_details/muc_config_form_provider.vala
src/ui/conversation_list_titlebar.vala
+ src/ui/global_search.vala
src/ui/conversation_selector/chat_row.vala
src/ui/conversation_selector/conversation_row.vala
src/ui/conversation_selector/groupchat_pm_row.vala
@@ -100,19 +104,16 @@ SOURCES
src/ui/conversation_selector/list.vala
src/ui/conversation_selector/view.vala
src/ui/conversation_summary/chat_state_populator.vala
+ src/ui/conversation_summary/content_item_widget_factory.vala
+ src/ui/conversation_summary/content_populator.vala
src/ui/conversation_summary/conversation_item_skeleton.vala
src/ui/conversation_summary/conversation_view.vala
src/ui/conversation_summary/date_separator_populator.vala
- src/ui/conversation_summary/default_file_display.vala
- src/ui/conversation_summary/default_message_display.vala
- src/ui/conversation_summary/file_populator.vala
- src/ui/conversation_summary/image_display.vala
- src/ui/conversation_summary/message_populator.vala
src/ui/conversation_summary/message_textview.vala
- src/ui/conversation_summary/slashme_message_display.vala
src/ui/conversation_summary/subscription_notification.vala
src/ui/conversation_titlebar/menu_entry.vala
src/ui/conversation_titlebar/occupants_entry.vala
+ src/ui/conversation_titlebar/search_entry.vala
src/ui/conversation_titlebar/view.vala
src/ui/manage_accounts/account_row.vala
src/ui/manage_accounts/add_account_dialog.vala
diff --git a/main/data/conversation_list_titlebar.ui b/main/data/conversation_list_titlebar.ui
index f8fabedc..6c5d2d0a 100644
--- a/main/data/conversation_list_titlebar.ui
+++ b/main/data/conversation_list_titlebar.ui
@@ -22,20 +22,5 @@
<property name="pack_type">start</property>
</packing>
</child>
- <child>
- <object class="GtkToggleButton" id="search_button">
- <property name="visible">True</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="icon-name">system-search-symbolic</property>
- <property name="icon-size">1</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="pack_type">end</property>
- </packing>
- </child>
</template>
</interface>
diff --git a/main/data/conversation_selector/view.ui b/main/data/conversation_selector/view.ui
index 365957a8..c5560ad1 100644
--- a/main/data/conversation_selector/view.ui
+++ b/main/data/conversation_selector/view.ui
@@ -5,21 +5,6 @@
<property name="expand">True</property>
<property name="orientation">vertical</property>
<child>
- <object class="GtkRevealer" id="search_revealer">
- <property name="hexpand">True</property>
- <property name="visible">True</property>
- <child>
- <object class="GtkSearchEntry" id="search_entry">
- <property name="primary_icon_name">edit-find-symbolic</property>
- <property name="placeholder_text" translatable="yes">Search</property>
- <property name="margin">10px</property>
- <property name="hexpand">True</property>
- <property name="visible">True</property>
- </object>
- </child>
- </object>
- </child>
- <child>
<object class="GtkScrolledWindow" id="scrolled">
<property name="expand">True</property>
<property name="hscrollbar_policy">never</property>
diff --git a/main/data/global_search.ui b/main/data/global_search.ui
new file mode 100644
index 00000000..44abf6de
--- /dev/null
+++ b/main/data/global_search.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoUiGlobalSearch" parent="GtkOverlay">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="results_empty_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-search-symbolic</property>
+ <property name="icon-size">4</property>
+ <property name="pixel-size">72</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">No active search</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ <attribute name="scale" value="1.3"/>
+ </attributes>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Type to start a search</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">empty</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">face-uncertain-symbolic</property>
+ <property name="icon-size">4</property>
+ <property name="pixel-size">72</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">No matching messages</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
+ <attribute name="scale" value="1.3"/>
+ </attributes>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Check the spelling or try to remove filters</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">no-result</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="entry_number_label">
+ <property name="xalign">0</property>
+ <property name="use-markup">True</property>
+ <property name="margin-left">17</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="results_scrolled">
+ <property name="hscrollbar-policy">never</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="results_box">
+ <property name="orientation">vertical</property>
+ <property name="spacing">25</property>
+ <property name="margin">10</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">results</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkFrame" id="auto_complete_overlay">
+ <property name="visible">True</property>
+ <property name="margin-top">42</property>
+ <property name="margin-left">12</property>
+ <property name="margin-right">12</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="auto-complete"/>
+ </style>
+ <child>
+ <object class="GtkListBox" id="auto_complete_list">
+ <property name="visible">True</property>
+ <property name="selection-mode">browse</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/main/data/menu_add.ui b/main/data/menu_add.ui
index d8fd691b..fdf01352 100644
--- a/main/data/menu_add.ui
+++ b/main/data/menu_add.ui
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_add">
<section>
diff --git a/main/data/menu_app.ui b/main/data/menu_app.ui
index beb81f3f..eb862ddb 100644
--- a/main/data/menu_app.ui
+++ b/main/data/menu_app.ui
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_app">
<section>
diff --git a/main/data/menu_conversation.ui b/main/data/menu_conversation.ui
index 42b580be..a65522c3 100644
--- a/main/data/menu_conversation.ui
+++ b/main/data/menu_conversation.ui
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_conversation">
<section>
diff --git a/main/data/search_autocomplete.ui b/main/data/search_autocomplete.ui
new file mode 100644
index 00000000..94ec5d7f
--- /dev/null
+++ b/main/data/search_autocomplete.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <object class="GtkBox" id="root">
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="DinoUiAvatarImage" id="image">
+ <property name="margin">4</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="height">24</property>
+ <property name="width">24</property>
+ <property name="visible">True</property>
+ <property name="allow_gray">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="ellipsize">end</property>
+ </object>
+ </child>
+ </object>
+</interface> \ No newline at end of file
diff --git a/main/data/theme.css b/main/data/theme.css
index e7d58ffb..226689b3 100644
--- a/main/data/theme.css
+++ b/main/data/theme.css
@@ -17,12 +17,47 @@ window.dino-main .dino-conversation undershoot {
background: none;
}
-window.dino-main .dino-chatinput frame box {
+@keyframes highlight {
+ from { background: alpha(@warning_color, 0.5) }
+ to { background: transparent }
+}
+
+window.dino-main .dino-conversation .highlight-once {
+ animation-duration: 3s;
+ animation-timing-function: ease-out;
+ animation-iteration-count: 1;
+ animation-name: highlight;
+}
+
+window.dino-main .dino-conversation textview, window.dino-main .dino-conversation textview text {
+ background: transparent;
+}
+
+window.dino-main .dino-sidebar > frame {
+ background: @insensitive_bg_color;
+ border-left: 1px solid @borders;
+ border-bottom: 1px solid @borders;
+}
+
+window.dino-main .dino-sidebar > frame.collapsed {
+ border-bottom: 1px solid @borders;
+}
+
+window.dino-main .dino-sidebar frame.auto-complete {
background: @theme_base_color;
}
-window.dino-main .dino-chatinput frame box:backdrop {
- background: @theme_unfocused_base_color;
+window.dino-main .dino-sidebar frame.auto-complete list > row {
+ transition: none;
+}
+
+window.dino-main .dino-sidebar textview,
+window.dino-main .dino-sidebar textview text {
+ background-color: transparent;
+}
+
+window.dino-main .dino-chatinput frame box {
+ background: transparent;
}
window.dino-main button.dino-chatinput-button {
diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui
new file mode 100644
index 00000000..b2f3a891
--- /dev/null
+++ b/main/data/unified_main_content.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <object class="GtkPaned" id="paned">
+ <property name="position">300</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="DinoUiConversationSelectorView" id="conversation_list">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dino-conversation"/>
+ </style>
+ <child>
+ <object class="DinoUiConversationSummaryConversationView" id="conversation_frame">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="DinoUiChatInputView" id="chat_input">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkRevealer" id="goto_end_revealer">
+ <property name="halign">end</property>
+ <property name="valign">end</property>
+ <property name="transition-type">crossfade</property>
+ <property name="visible">True</property>
+ <property name="margin-end">30</property>
+ <property name="margin-bottom">70</property>
+ <child>
+ <object class="GtkButton" id="goto_end_button">
+ <property name="vexpand">False</property>
+ <property name="halign">end</property>
+ <property name="valign">end</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="circular"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">go-down-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkRevealer" id="search_revealer">
+ <property name="visible">True</property>
+ <property name="halign">end</property>
+ <property name="transition-type">slide-left</property>
+ <style>
+ <class name="dino-sidebar"/>
+ </style>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="width-request">400</property>
+ <property name="shadow-type">none</property>
+ <child>
+ <object class="DinoUiGlobalSearch" id="search_box">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala
index 22d6d93d..86a4e288 100644
--- a/main/src/ui/application.vala
+++ b/main/src/ui/application.vala
@@ -32,7 +32,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
window = new UnifiedWindow(this, stream_interactor);
notifications = new Notifications(stream_interactor, window);
notifications.start();
- notifications.conversation_selected.connect(window.on_conversation_selected);
+ notifications.conversation_selected.connect((conversation) => window.on_conversation_selected(conversation));
}
window.present();
});
diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala
index a1c2b83d..dd111997 100644
--- a/main/src/ui/chat_input/view.vala
+++ b/main/src/ui/chat_input/view.vala
@@ -32,7 +32,7 @@ public class View : Box {
[GtkChild] private Separator file_separator;
private EncryptionButton encryption_widget = new EncryptionButton() { margin_top=3, valign=Align.START, visible=true };
- public View(StreamInteractor stream_interactor) {
+ public View init(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input);
@@ -70,6 +70,7 @@ public class View : Box {
Util.force_css(frame, "* { border-radius: 3px; }");
stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available);
+ return this;
}
public void initialize_for_conversation(Conversation conversation) {
diff --git a/main/src/ui/conversation_list_titlebar.vala b/main/src/ui/conversation_list_titlebar.vala
index 65515019..60d9a6fb 100644
--- a/main/src/ui/conversation_list_titlebar.vala
+++ b/main/src/ui/conversation_list_titlebar.vala
@@ -10,7 +10,6 @@ public class ConversationListTitlebar : Gtk.HeaderBar {
public signal void conversation_opened(Conversation conversation);
[GtkChild] private MenuButton add_button;
- [GtkChild] public ToggleButton search_button;
private StreamInteractor stream_interactor;
diff --git a/main/src/ui/conversation_selector/view.vala b/main/src/ui/conversation_selector/view.vala
index b6b02848..d06ad133 100644
--- a/main/src/ui/conversation_selector/view.vala
+++ b/main/src/ui/conversation_selector/view.vala
@@ -10,43 +10,14 @@ namespace Dino.Ui.ConversationSelector {
public class View : Box {
public List conversation_list;
- [GtkChild] public SearchEntry search_entry;
- [GtkChild] public Revealer search_revealer;
[GtkChild] private ScrolledWindow scrolled;
- public View(StreamInteractor stream_interactor) {
+ public View init(StreamInteractor stream_interactor) {
conversation_list = new List(stream_interactor) { visible=true };
scrolled.add(conversation_list);
- search_entry.key_release_event.connect(search_key_release_event);
- search_entry.search_changed.connect(search_changed);
+ return this;
}
- public void conversation_selected(Conversation? conversation) {
- search_entry.set_text("");
- }
-
- private void refilter() {
- string[]? values = null;
- string str = search_entry.get_text ();
- if (str != "") values = str.split(" ");
- conversation_list.set_filter_values(values);
- }
-
- private void search_changed(Editable editable) {
- refilter();
- }
-
- private bool search_key_release_event(EventKey event) {
- conversation_list.select_row(conversation_list.get_row_at_y(0));
- if (event.keyval == Key.Down) {
- ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0);
- if (row != null) {
- conversation_list.select_row(row);
- row.grab_focus();
- }
- }
- return false;
- }
}
}
diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala
index 1ea52a6d..d07ab743 100644
--- a/main/src/ui/conversation_summary/chat_state_populator.vala
+++ b/main/src/ui/conversation_summary/chat_state_populator.vala
@@ -6,7 +6,7 @@ using Xmpp;
namespace Dino.Ui.ConversationSummary {
-class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
+class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
public string id { get { return "chat_state"; } }
@@ -43,8 +43,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
- public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
-
private void update_chat_state(Account account, Jid jid) {
HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation);
diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala
new file mode 100644
index 00000000..8a2bf136
--- /dev/null
+++ b/main/src/ui/conversation_summary/content_item_widget_factory.vala
@@ -0,0 +1,224 @@
+using Gee;
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class ContentItemWidgetFactory : Object {
+
+ private StreamInteractor stream_interactor;
+ private HashMap<string, WidgetGenerator> generators = new HashMap<string, WidgetGenerator>();
+
+ public ContentItemWidgetFactory(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor);
+ generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor);
+ }
+
+ public Widget? get_widget(ContentItem item) {
+ WidgetGenerator? generator = generators[item.type_];
+ if (generator != null) {
+ return (Widget?) generator.get_widget(item);
+ }
+ return null;
+ }
+
+ public void register_widget_generator(WidgetGenerator generator) {
+ generators[generator.handles_type] = generator;
+ }
+}
+
+public interface WidgetGenerator : Object {
+ public abstract string handles_type { get; set; }
+ public abstract Object get_widget(ContentItem item);
+}
+
+public class MessageItemWidgetGenerator : WidgetGenerator, Object {
+
+ public string handles_type { get; set; default=FileItem.TYPE; }
+
+ private StreamInteractor stream_interactor;
+
+ public MessageItemWidgetGenerator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public Object get_widget(ContentItem item) {
+ MessageItem message_item = item as MessageItem;
+ Conversation conversation = message_item.conversation;
+ Message message = message_item.message;
+
+ MessageTextView text_view = new MessageTextView() { vexpand=true, visible = true };
+
+ if (message_item.message.body.has_prefix("/me")) {
+ text_view.add_text(message.body.substring(3));
+ } else {
+ text_view.add_text(message.body);
+ }
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ text_view.highlight_word(conversation.nickname);
+ }
+ if (message_item.message.body.has_prefix("/me")) {
+ string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+ string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view));
+ TextTag nick_tag = text_view.buffer.create_tag(null, foreground: @"#$color");
+ TextIter iter;
+ text_view.buffer.get_start_iter(out iter);
+ text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
+
+ text_view.style_updated.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view));
+ text_view.realize.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view));
+ }
+ return text_view;
+ }
+
+ public static void update_style(StreamInteractor stream_interactor, Message message, Conversation conversation, TextTag nick_tag, TextView text_view) {
+ string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view));
+ nick_tag.foreground = "#" + color;
+ }
+}
+
+public class FileItemWidgetGenerator : WidgetGenerator, Object {
+
+ public StreamInteractor stream_interactor;
+ public string handles_type { get; set; default=FileItem.TYPE; }
+
+ private const int MAX_HEIGHT = 300;
+ private const int MAX_WIDTH = 600;
+
+ public FileItemWidgetGenerator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public Object get_widget(ContentItem item) {
+ FileItem file_item = item as FileItem;
+ FileTransfer transfer = file_item.file_transfer;
+ if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) {
+ return getImageWidget(transfer);
+ } else {
+ return getDefaultWidget(transfer);
+ }
+ }
+
+ private Widget getImageWidget(FileTransfer file_transfer) {
+ Image image = new Image() { halign=Align.START, visible = true };
+ Gdk.Pixbuf pixbuf;
+ try {
+ pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path());
+ } catch (Error error) {
+ return null;
+ }
+
+ int max_scaled_height = MAX_HEIGHT * image.scale_factor;
+ if (pixbuf.height > max_scaled_height) {
+ pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR);
+ }
+ int max_scaled_width = MAX_WIDTH * image.scale_factor;
+ if (pixbuf.width > max_scaled_width) {
+ pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR);
+ }
+ pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor());
+ Util.image_set_from_scaled_pixbuf(image, pixbuf);
+ Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }");
+
+ Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui");
+ Widget toolbar = builder.get_object("main") as Widget;
+ Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)");
+ Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }");
+
+ Label url_label = builder.get_object("url_label") as Label;
+ Util.force_color(url_label, "#eee");
+
+ if (file_transfer.file_name != null && file_transfer.file_name != "") {
+ string caption = file_transfer.file_name;
+ url_label.label = caption;
+ } else {
+ url_label.visible = false;
+ }
+
+ Image open_image = builder.get_object("open_image") as Image;
+ Util.force_css(open_image, "*:not(:hover) { color: #eee; }");
+ Button open_button = builder.get_object("open_button") as Button;
+ Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }");
+ open_button.clicked.connect(() => {
+ try{
+ AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
+ } catch (Error err) {
+ print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n");
+ }
+ });
+
+ Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true };
+ toolbar_revealer.add(toolbar);
+
+ Grid grid = new Grid() { visible=true };
+ grid.attach(toolbar_revealer, 0, 0, 1, 1);
+ grid.attach(image, 0, 0, 1, 1);
+
+ EventBox event_box = new EventBox() { halign=Align.START, visible=true };
+ event_box.add(grid);
+ event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; });
+ event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; });
+
+ return event_box;
+ }
+
+ private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) {
+ Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height));
+ Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0);
+ double degrees = Math.PI / 180.0;
+ ctx.new_sub_path();
+ ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees);
+ ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees);
+ ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees);
+ ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees);
+ ctx.close_path();
+ ctx.clip();
+ ctx.paint();
+ return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height);
+ }
+
+ private Widget getDefaultWidget(FileTransfer file_transfer) {
+ Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true };
+ string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type);
+ Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true };
+ main_box.add(content_type_image);
+
+ Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true };
+ Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true};
+ right_box.add(name_label);
+ Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true};
+ mime_label.get_style_context().add_class("dim-label");
+ right_box.add(mime_label);
+ main_box.add(right_box);
+
+ EventBox event_box = new EventBox() { halign=Align.START, visible=true };
+ event_box.add(main_box);
+
+ event_box.enter_notify_event.connect((event) => {
+ event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2));
+ return false;
+ });
+ event_box.leave_notify_event.connect((event) => {
+ event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM));
+ return false;
+ });
+ event_box.button_release_event.connect((event_button) => {
+ if (event_button.button == 1) {
+ try{
+ AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
+ } catch (Error err) {
+ print("Tried to open " + file_transfer.get_file().get_path());
+ }
+ }
+ return false;
+ });
+
+ return event_box;
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala
new file mode 100644
index 00000000..9ebb9159
--- /dev/null
+++ b/main/src/ui/conversation_summary/content_populator.vala
@@ -0,0 +1,110 @@
+using Gee;
+using Gtk;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class ContentProvider : ContentItemCollection, Object {
+
+ private StreamInteractor stream_interactor;
+ private ContentItemWidgetFactory widget_factory;
+ private Conversation? current_conversation;
+ private Plugins.ConversationItemCollection? item_collection;
+
+ public ContentProvider(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ this.widget_factory = new ContentItemWidgetFactory(stream_interactor);
+ }
+
+ public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) {
+ if (current_conversation != null) {
+ stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this);
+ }
+ current_conversation = conversation;
+ this.item_collection = item_collection;
+ stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this);
+ }
+
+ public void insert_item(ContentItem item) {
+ item_collection.insert_item(new ContentMetaItem(item, widget_factory));
+ }
+
+ public void remove_item(ContentItem item) { }
+
+
+ public Gee.List<ContentMetaItem> populate_latest(Conversation conversation, int n) {
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation, n);
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public Gee.List<ContentMetaItem> populate_before(Conversation conversation, ContentItem before_item, int n) {
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n);
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public Gee.List<ContentMetaItem> populate_after(Conversation conversation, ContentItem after_item, int n) {
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n);
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public ContentMetaItem get_content_meta_item(ContentItem content_item) {
+ return new ContentMetaItem(content_item, widget_factory);
+ }
+}
+
+public class ContentMetaItem : Plugins.MetaConversationItem {
+ public override Jid? jid { get; set; }
+ public override DateTime? sort_time { get; set; }
+ public override DateTime? display_time { get; set; }
+ public override Encryption? encryption { get; set; }
+
+ public ContentItem content_item;
+ private ContentItemWidgetFactory widget_factory;
+
+ public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) {
+ this.jid = content_item.jid;
+ this.sort_time = content_item.sort_time;
+ this.seccondary_sort_indicator = content_item.seccondary_sort_indicator;
+ this.display_time = content_item.display_time;
+ this.encryption = content_item.encryption;
+ this.mark = content_item.mark;
+
+ WeakRef weak_item = WeakRef(content_item);
+ content_item.notify["mark"].connect(() => {
+ ContentItem? ci = weak_item.get() as ContentItem;
+ if (ci == null) return;
+ this.mark = ci.mark;
+ });
+
+ this.can_merge = true;
+ this.requires_avatar = true;
+ this.requires_header = true;
+
+ this.content_item = content_item;
+ this.widget_factory = widget_factory;
+ }
+
+ public override bool can_merge { get; set; default=true; }
+ public override bool requires_avatar { get; set; default=true; }
+ public override bool requires_header { get; set; default=true; }
+
+ public override Object? get_widget(Plugins.WidgetType type) {
+ return widget_factory.get_widget(content_item);
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala
index a8da93ef..a4e45f7a 100644
--- a/main/src/ui/conversation_summary/conversation_item_skeleton.vala
+++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala
@@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box {
return datetime.format(format);
}
- public virtual string get_relative_time(DateTime datetime) {
+ public static string get_relative_time(DateTime datetime) {
DateTime now = new DateTime.now_local();
TimeSpan timespan = now.difference(datetime);
if (timespan > 365 * TimeSpan.DAY) {
diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala
index fac53b7d..e6a564de 100644
--- a/main/src/ui/conversation_summary/conversation_view.vala
+++ b/main/src/ui/conversation_summary/conversation_view.vala
@@ -11,19 +11,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
public Conversation? conversation { get; private set; }
- [GtkChild] private ScrolledWindow scrolled;
+ [GtkChild] public ScrolledWindow scrolled;
[GtkChild] private Revealer notification_revealer;
[GtkChild] private Box notifications;
[GtkChild] private Box main;
[GtkChild] private Stack stack;
private StreamInteractor stream_interactor;
- private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items);
- private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>();
+ private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
+ private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
- private MessagePopulator message_item_populator;
+ private ContentProvider content_populator;
private SubscriptionNotitication subscription_notification;
private double? was_value;
@@ -33,22 +33,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
private Mutex reloading_mutex = Mutex();
private bool animate = false;
private bool firstLoad = true;
+ private bool at_current_content = true;
+ private bool reload_messages = true;
- public ConversationView(StreamInteractor stream_interactor) {
+ public ConversationView init(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
scrolled.vadjustment.notify["value"].connect(on_value_notify);
- message_item_populator = new MessagePopulator(stream_interactor);
+ content_populator = new ContentProvider(stream_interactor);
subscription_notification = new SubscriptionNotitication(stream_interactor);
- insert_item.connect(on_insert_item);
- remove_item.connect(on_remove_item);
+ insert_item.connect(filter_insert_item);
+ remove_item.connect(do_remove_item);
Application app = GLib.Application.get_default() as Application;
- app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor));
- app.plugin_registry.register_conversation_item_populator(new FilePopulator(stream_interactor));
- app.plugin_registry.register_conversation_item_populator(new DateSeparatorPopulator(stream_interactor));
+ app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
+ app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor));
Timeout.add_seconds(60, () => {
foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
@@ -57,7 +58,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
return true;
});
- Util.force_base_background(this);
+ return this;
}
// Workaround GTK TextView issues: Delay first load of contents
@@ -65,54 +66,127 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
if (firstLoad) {
int timeout = firstLoad ? 1000 : 0;
Timeout.add(timeout, () => {
+ stack.set_visible_child_name("void");
initialize_for_conversation_(conversation);
+ display_latest();
+ stack.set_visible_child_name("main");
return false;
});
firstLoad = false;
} else {
+ stack.set_visible_child_name("void");
initialize_for_conversation_(conversation);
+ display_latest();
+ stack.set_visible_child_name("main");
}
+ }
+ public void initialize_around_message(Conversation conversation, ContentItem content_item) {
+ stack.set_visible_child_name("void");
+ clear();
+ initialize_for_conversation_(conversation);
+ Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
+ foreach (ContentMetaItem item in before_items) {
+ do_insert_item(item);
+ }
+ ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item);
+ meta_item.can_merge = false;
+ Widget w = insert_new(meta_item);
+ content_items.add(meta_item);
+ meta_items.add(meta_item);
+
+ Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40);
+ foreach (ContentMetaItem item in after_items) {
+ do_insert_item(item);
+ }
+ if (after_items.size == 40) {
+ at_current_content = false;
+ }
+ {
+ int h = 0, i = 0;
+ main.@foreach((widget) => {
+ if (i >= before_items.size) return;
+ ConversationItemSkeleton? sk = widget as ConversationItemSkeleton;
+ i += sk != null ? sk.items.size : 1;
+ int minimum_height, natural_height;
+ widget.get_preferred_height_for_width(main.get_allocated_width() - 2 * main.margin, out minimum_height, out natural_height);
+ h += minimum_height + 15;
+ });
+ }
+
+ reload_messages = false;
+ Timeout.add(700, () => {
+ int h = 0, i = 0;
+ main.@foreach((widget) => {
+ if (i >= before_items.size) return;
+ ConversationItemSkeleton? sk = widget as ConversationItemSkeleton;
+ i += sk != null ? sk.items.size : 1;
+ h += widget.get_allocated_height() + 15;
+ });
+ scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
+ w.get_style_context().add_class("highlight-once");
+ reload_messages = true;
+ stack.set_visible_child_name("main");
+ return false;
+ });
}
private void initialize_for_conversation_(Conversation? conversation) {
Dino.Application app = Dino.Application.get_default();
if (this.conversation != null) {
- foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
+ foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.close(conversation);
}
}
this.conversation = conversation;
- stack.set_visible_child_name("void");
- clear();
- was_upper = null;
- was_page_size = null;
+
+ foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
+ populator.init(conversation, this, Plugins.WidgetType.GTK);
+ }
+ content_populator.init(this, conversation, Plugins.WidgetType.GTK);
+ subscription_notification.init(conversation, this);
+
animate = false;
Timeout.add(20, () => { animate = true; return false; });
+ }
- foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
- populator.init(conversation, this, Plugins.WidgetType.GTK);
+ private void display_latest() {
+ clear();
+
+ Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40);
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
}
- message_item_populator.init(conversation, this);
- message_item_populator.populate_latest(conversation, 40);
Idle.add(() => { on_value_notify(); return false; });
+ }
- subscription_notification.init(conversation, this);
-
- stack.set_visible_child_name("main");
+ public void filter_insert_item(Plugins.MetaConversationItem item) {
+ if (meta_items.size > 0) {
+ bool after_last = meta_items.last().sort_time.compare(item.sort_time) < 0;
+ bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0;
+ bool accept = within_range || (at_current_content && after_last);
+ if (!accept) {
+ return;
+ }
+ }
+ do_insert_item(item);
}
- public void on_insert_item(Plugins.MetaConversationItem item) {
+ public void do_insert_item(Plugins.MetaConversationItem item) {
lock (meta_items) {
if (!item.can_merge || !merge_back(item)) {
insert_new(item);
}
}
+ if (item as ContentMetaItem != null) {
+ content_items.add(item);
+ }
+ meta_items.add(item);
}
- public void on_remove_item(Plugins.MetaConversationItem item) {
- lock (meta_items) {
- ConversationItemSkeleton? skeleton = item_item_skeletons[item];
+ private void do_remove_item(Plugins.MetaConversationItem item) {
+ ConversationItemSkeleton? skeleton = item_item_skeletons[item];
+ if (skeleton != null) {
if (skeleton.items.size > 1) {
skeleton.remove_meta_item(item);
} else {
@@ -122,6 +196,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
item_skeletons.remove(skeleton);
item_item_skeletons.unset(item);
}
+ content_items.remove(item);
meta_items.remove(item);
}
}
@@ -151,10 +226,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
lower_start_item.encryption == item.encryption &&
(item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) {
lower_skeleton.add_meta_item(item);
- force_alloc_width(lower_skeleton, main.get_allocated_width());
+ Util.force_alloc_width(lower_skeleton, main.get_allocated_width());
+ widgets[item] = widgets[lower_start_item];
item_item_skeletons[item] = lower_skeleton;
- meta_items.add(item);
return true;
}
@@ -162,7 +237,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
return false;
}
- private void insert_new(Plugins.MetaConversationItem item) {
+ private Widget insert_new(Plugins.MetaConversationItem item) {
Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
// Does another skeleton need to be split?
@@ -181,7 +256,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
item_item_skeletons[item] = item_skeleton;
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
item_skeletons.insert(index, item_skeleton);
- meta_items.add(item);
// Insert widget
Widget insert = item_skeleton;
@@ -195,22 +269,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
main.add(insert);
}
widgets[item] = insert;
- force_alloc_width(insert, main.get_allocated_width());
+ Util.force_alloc_width(insert, main.get_allocated_width());
main.reorder_child(insert, index);
// If an item from the past was added, add everything between that item and the (post-)first present item
if (index == 0) {
Dino.Application app = Dino.Application.get_default();
if (item_skeletons.size == 1) {
- foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
+ foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc());
}
} else {
- foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
+ foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time);
}
}
}
+ return insert;
}
private void split_at_time(ConversationItemSkeleton split_skeleton, DateTime time) {
@@ -219,12 +294,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
while(i < split_skeleton.items.size) {
Plugins.MetaConversationItem meta_item = split_skeleton.items[i];
if (time.compare(meta_item.display_time) < 0) {
- remove_item(meta_item);
+ do_remove_item(meta_item);
if (!already_divided) {
insert_new(meta_item);
already_divided = true;
} else {
- insert_item(meta_item);
+ do_insert_item(meta_item);
}
}
i++;
@@ -232,51 +307,73 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
}
private void on_upper_notify() {
- if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 ||
- scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
- scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
+ if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
+ if (at_current_content) {
+ scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
+ }
} else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
}
was_upper = scrolled.vadjustment.upper;
was_page_size = scrolled.vadjustment.page_size;
+ was_value = scrolled.vadjustment.value;
reloading_mutex.trylock();
reloading_mutex.unlock();
}
private void on_value_notify() {
- if (scrolled.vadjustment.value < 200) {
+ if (scrolled.vadjustment.value < 400) {
load_earlier_messages();
+ } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
+ load_later_messages();
}
}
private void load_earlier_messages() {
was_value = scrolled.vadjustment.value;
if (!reloading_mutex.trylock()) return;
- if (meta_items.size > 0) message_item_populator.populate_before(conversation, meta_items.first(), 20);
+ if (meta_items.size > 0) {
+ Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20);
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ } else {
+ reloading_mutex.unlock();
+ }
}
- private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
+ private void load_later_messages() {
+ if (!reloading_mutex.trylock()) return;
+ if (meta_items.size > 0 && !at_current_content) {
+ Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20);
+ if (items.size == 0) {
+ at_current_content = true;
+ }
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ } else {
+ reloading_mutex.unlock();
+ }
+ }
+
+ private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
int res = a.sort_time.compare(b.sort_time);
if (res == 0) {
- if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1;
- else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) res = 1;
+ if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) {
+ res = -1;
+ } else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) {
+ res = 1;
+ }
}
return res;
}
- // Workaround GTK TextView issues
- private void force_alloc_width(Widget widget, int width) {
- Allocation alloc = Allocation();
- widget.get_preferred_width(out alloc.width, null);
- widget.get_preferred_height(out alloc.height, null);
- alloc.width = width;
- widget.size_allocate(alloc);
- }
-
private void clear() {
+ was_upper = null;
+ was_page_size = null;
+ content_items.clear();
meta_items.clear();
- meta_after_items.clear();
item_skeletons.clear();
item_item_skeletons.clear();
widgets.clear();
diff --git a/main/src/ui/conversation_summary/date_separator_populator.vala b/main/src/ui/conversation_summary/date_separator_populator.vala
index 34005ab6..6a1ba782 100644
--- a/main/src/ui/conversation_summary/date_separator_populator.vala
+++ b/main/src/ui/conversation_summary/date_separator_populator.vala
@@ -6,7 +6,7 @@ using Xmpp;
namespace Dino.Ui.ConversationSummary {
-class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object {
+class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
public string id { get { return "date_separator"; } }
@@ -35,8 +35,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object {
public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { }
- public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
-
private void on_insert_item(Plugins.MetaConversationItem item) {
if (item.display_time == null) return;
diff --git a/main/src/ui/conversation_summary/default_file_display.vala b/main/src/ui/conversation_summary/default_file_display.vala
deleted file mode 100644
index 1547440b..00000000
--- a/main/src/ui/conversation_summary/default_file_display.vala
+++ /dev/null
@@ -1,95 +0,0 @@
-using Gdk;
-using Gtk;
-
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class DefaultFileDisplay : Plugins.MetaConversationItem {
- public override Jid? jid { get; set; }
- public override DateTime? sort_time { get; set; }
- public override DateTime? display_time { get; set; }
- public override Encryption? encryption { get; set; }
- public override Entities.Message.Marked? mark { get; set; }
-
- public override bool can_merge { get; set; default=true; }
- public override bool requires_avatar { get; set; default=true; }
- public override bool requires_header { get; set; default=true; }
-
- private const int MAX_HEIGHT = 300;
- private const int MAX_WIDTH = 600;
-
- private StreamInteractor stream_interactor;
- private FileTransfer file_transfer;
-
- public DefaultFileDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) {
- this.stream_interactor = stream_interactor;
- this.file_transfer = file_transfer;
-
- this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart;
- this.sort_time = file_transfer.time;
- this.seccondary_sort_indicator = file_transfer.id + 0.2903;
- this.display_time = file_transfer.time;
- this.encryption = file_transfer.encryption;
- this.mark = file_to_message_state(file_transfer.state);
- file_transfer.notify["state"].connect_after(() => {
- this.mark = file_to_message_state(file_transfer.state);
- });
- }
-
- public override Object? get_widget(Plugins.WidgetType widget_type) {
- Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true };
- string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type);
- Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true };
- main_box.add(content_type_image);
-
- Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true };
- Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true};
- right_box.add(name_label);
- Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true};
- mime_label.get_style_context().add_class("dim-label");
- right_box.add(mime_label);
- main_box.add(right_box);
-
- EventBox event_box = new EventBox() { halign=Align.START, visible=true };
- event_box.add(main_box);
-
- event_box.enter_notify_event.connect((event) => {
- event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2));
- return false;
- });
- event_box.leave_notify_event.connect((event) => {
- event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM));
- return false;
- });
- event_box.button_release_event.connect((event_button) => {
- if (event_button.button == 1) {
- try{
- AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
- } catch (Error err) {
- print("Tried to open " + file_transfer.get_file().get_path());
- }
- }
- return false;
- });
-
- return event_box;
- }
-
- private Entities.Message.Marked file_to_message_state(FileTransfer.State state) {
- switch (state) {
- case FileTransfer.State.IN_PROCESS:
- return Entities.Message.Marked.UNSENT;
- case FileTransfer.State.COMPLETE:
- return Entities.Message.Marked.NONE;
- case FileTransfer.State.NOT_STARTED:
- return Entities.Message.Marked.UNSENT;
- case FileTransfer.State.FAILED:
- return Entities.Message.Marked.WONTSEND;
- }
- assert_not_reached();
- }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/default_message_display.vala b/main/src/ui/conversation_summary/default_message_display.vala
deleted file mode 100644
index 519e5107..00000000
--- a/main/src/ui/conversation_summary/default_message_display.vala
+++ /dev/null
@@ -1,58 +0,0 @@
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class DefaultMessageDisplay : Plugins.MessageDisplayProvider, Object {
- public string id { get; set; default="default"; }
- public double priority { get; set; default=0; }
-
- public StreamInteractor stream_interactor;
-
- public DefaultMessageDisplay(StreamInteractor stream_interactor) {
- this.stream_interactor = stream_interactor;
- }
-
- public bool can_display(Entities.Message? message) { return true; }
-
- public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
- return new MetaMessageItem(stream_interactor, message, conversation);
- }
-}
-
-public class MetaMessageItem : Plugins.MetaConversationItem {
- public override Jid? jid { get; set; }
- public override DateTime? sort_time { get; set; }
- public override DateTime? display_time { get; set; }
- public override Encryption? encryption { get; set; }
-
- private StreamInteractor stream_interactor;
- private Conversation conversation;
- private Message message;
-
- public MetaMessageItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
- this.stream_interactor = stream_interactor;
- this.conversation = conversation;
- this.message = message;
- this.jid = message.from;
- this.sort_time = message.local_time;
- this.seccondary_sort_indicator = message.id + 0.2085;
- this.display_time = message.time;
- this.encryption = message.encryption;
- }
-
- public override bool can_merge { get; set; default=true; }
- public override bool requires_avatar { get; set; default=true; }
- public override bool requires_header { get; set; default=true; }
-
- public override Object? get_widget(Plugins.WidgetType widget_type) {
- MessageTextView text_view = new MessageTextView() { visible = true };
- text_view.add_text(message.body);
- if (conversation.type_ == Conversation.Type.GROUPCHAT) {
- text_view.highlight_word(conversation.nickname);
- }
- return text_view;
- }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/file_populator.vala b/main/src/ui/conversation_summary/file_populator.vala
deleted file mode 100644
index af7bc992..00000000
--- a/main/src/ui/conversation_summary/file_populator.vala
+++ /dev/null
@@ -1,54 +0,0 @@
-using Gee;
-using Gtk;
-
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-class FilePopulator : Plugins.ConversationItemPopulator, Object {
-
- public string id { get { return "file"; } }
-
- private StreamInteractor? stream_interactor;
- private Conversation? current_conversation;
- private Plugins.ConversationItemCollection? item_collection;
-
- public FilePopulator(StreamInteractor stream_interactor) {
- this.stream_interactor = stream_interactor;
-
- stream_interactor.get_module(FileManager.IDENTITY).received_file.connect((file_transfer) => {
- if (current_conversation != null && current_conversation.account.equals(file_transfer.account) && current_conversation.counterpart.equals_bare(file_transfer.counterpart)) {
- insert_file(file_transfer);
- }
- });
- }
-
- public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) {
- current_conversation = conversation;
- this.item_collection = item_collection;
- }
-
- public void close(Conversation conversation) { }
-
- public void populate_timespan(Conversation conversation, DateTime from, DateTime to) {
- Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_file_transfers(conversation.account, conversation.counterpart, from, to);
- foreach (FileTransfer transfer in transfers) {
- insert_file(transfer);
- }
- }
-
- public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
-
- private void insert_file(FileTransfer transfer) {
- Plugins.MetaConversationItem item = null;
- if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) {
- item = new ImageDisplay(stream_interactor, transfer);
- } else {
- item = new DefaultFileDisplay(stream_interactor, transfer);
- }
- item_collection.insert_item(item);
- }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/image_display.vala b/main/src/ui/conversation_summary/image_display.vala
deleted file mode 100644
index 15880836..00000000
--- a/main/src/ui/conversation_summary/image_display.vala
+++ /dev/null
@@ -1,137 +0,0 @@
-using Gtk;
-
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class ImageDisplay : Plugins.MetaConversationItem {
- public override Jid? jid { get; set; }
- public override DateTime? sort_time { get; set; }
- public override DateTime? display_time { get; set; }
- public override Encryption? encryption { get; set; }
- public override Entities.Message.Marked? mark { get; set; }
-
- public override bool can_merge { get; set; default=true; }
- public override bool requires_avatar { get; set; default=true; }
- public override bool requires_header { get; set; default=true; }
-
- private const int MAX_HEIGHT = 300;
- private const int MAX_WIDTH = 600;
-
- private StreamInteractor stream_interactor;
- private FileTransfer file_transfer;
-
- public ImageDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) {
- this.stream_interactor = stream_interactor;
- this.file_transfer = file_transfer;
-
- this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart;
- this.sort_time = file_transfer.time;
- this.seccondary_sort_indicator = file_transfer.id + 0.2903;
- this.display_time = file_transfer.time;
- this.encryption = file_transfer.encryption;
- this.mark = file_to_message_state(file_transfer.state);
- file_transfer.notify["state"].connect_after(() => {
- this.mark = file_to_message_state(file_transfer.state);
- });
- }
-
- public override Object? get_widget(Plugins.WidgetType widget_type) {
- Image image = new Image() { halign=Align.START, visible = true };
- Gdk.Pixbuf pixbuf;
- try {
- pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path());
- } catch (Error error) {
- return null;
- }
-
- int max_scaled_height = MAX_HEIGHT * image.scale_factor;
- if (pixbuf.height > max_scaled_height) {
- pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR);
- }
- int max_scaled_width = MAX_WIDTH * image.scale_factor;
- if (pixbuf.width > max_scaled_width) {
- pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR);
- }
- pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor());
- Util.image_set_from_scaled_pixbuf(image, pixbuf);
- Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }");
-
- Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui");
- Widget toolbar = builder.get_object("main") as Widget;
- Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)");
- Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }");
-
- Label url_label = builder.get_object("url_label") as Label;
- Util.force_color(url_label, "#eee");
- update_info(url_label, file_transfer.file_name);
-
- Image open_image = builder.get_object("open_image") as Image;
- Util.force_css(open_image, "*:not(:hover) { color: #eee; }");
- Button open_button = builder.get_object("open_button") as Button;
- Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }");
- open_button.clicked.connect(() => {
- try{
- AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
- } catch (Error err) {
- print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n");
- }
- });
-
- Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true };
- toolbar_revealer.add(toolbar);
-
- Grid grid = new Grid() { visible=true };
- grid.attach(toolbar_revealer, 0, 0, 1, 1);
- grid.attach(image, 0, 0, 1, 1);
-
- EventBox event_box = new EventBox() { halign=Align.START, visible=true };
- event_box.add(grid);
- event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; });
- event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; });
-
- return event_box;
- }
-
- private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) {
- Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height));
- Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0);
- double degrees = Math.PI / 180.0;
- ctx.new_sub_path();
- ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees);
- ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees);
- ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees);
- ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees);
- ctx.close_path();
- ctx.clip();
- ctx.paint();
- return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height);
- }
-
- private void update_info(Label url_label, string? info) {
- string url = info ?? "";
- if (url.has_prefix("https://")) url = url.substring(8);
- if (url.has_prefix("http://")) url = url.substring(7);
- if (url.has_prefix("www.")) url = url.substring(4);
- string[] slash_split = url.split("/");
- if (slash_split.length > 2) url = slash_split[0] + "/…/" + slash_split[slash_split.length - 1];
- url_label.label = url;
- }
-
- private Entities.Message.Marked file_to_message_state(FileTransfer.State state) {
- switch (state) {
- case FileTransfer.State.IN_PROCESS:
- return Entities.Message.Marked.UNSENT;
- case FileTransfer.State.COMPLETE:
- return Entities.Message.Marked.NONE;
- case FileTransfer.State.NOT_STARTED:
- return Entities.Message.Marked.UNSENT;
- case FileTransfer.State.FAILED:
- return Entities.Message.Marked.WONTSEND;
- }
- assert_not_reached();
- }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/message_populator.vala b/main/src/ui/conversation_summary/message_populator.vala
deleted file mode 100644
index b342306b..00000000
--- a/main/src/ui/conversation_summary/message_populator.vala
+++ /dev/null
@@ -1,81 +0,0 @@
-using Gee;
-using Gtk;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class MessagePopulator : Object {
-
- private StreamInteractor? stream_interactor;
- private Conversation? current_conversation;
- private Plugins.ConversationItemCollection? item_collection;
- private HashMap<Plugins.MetaConversationItem, Message> meta_message = new HashMap<Plugins.MetaConversationItem, Message>();
-
- public MessagePopulator(StreamInteractor stream_interactor) {
- this.stream_interactor = stream_interactor;
-
- Application app = GLib.Application.get_default() as Application;
- app.plugin_registry.register_message_display(new DefaultMessageDisplay(stream_interactor));
- app.plugin_registry.register_message_display(new SlashmeMessageDisplay(stream_interactor));
-
-
- stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(handle_message);
- stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_message);
- }
-
- public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection) {
- current_conversation = conversation;
- this.item_collection = item_collection;
- }
-
- public void close(Conversation conversation) { }
-
- public void populate_latest(Conversation conversation, int n) {
- Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation, n);
- if (messages != null) {
- foreach (Entities.Message message in messages) {
- handle_message(message, conversation);
- }
- }
- }
-
- public void populate_before(Conversation conversation, Plugins.MetaConversationItem item, int n) {
- Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, meta_message[item], n);
- if (messages != null) {
- foreach (Entities.Message message in messages) {
- handle_message(message, conversation);
- }
- }
- }
-
- private void handle_message(Message message, Conversation conversation) {
- if (!conversation.equals(current_conversation)) return;
-
- Plugins.MessageDisplayProvider? best_provider = null;
- double priority = -1;
- Application app = GLib.Application.get_default() as Application;
- foreach (Plugins.MessageDisplayProvider provider in app.plugin_registry.message_displays) {
- if (provider.can_display(message) && provider.priority > priority) {
- best_provider = provider;
- priority = provider.priority;
- }
- }
- Plugins.MetaConversationItem? meta_item = best_provider.get_item(message, conversation);
- if (meta_item == null) return;
- meta_message[meta_item] = message;
-
- meta_item.mark = message.marked;
- WeakRef weak_meta_item = WeakRef(meta_item);
- WeakRef weak_message = WeakRef(message);
- message.notify["marked"].connect(() => {
- Plugins.MetaConversationItem? mi = weak_meta_item.get() as Plugins.MetaConversationItem;
- Message? m = weak_message.get() as Message;
- if (mi == null || m == null) return;
- mi.mark = m.marked;
- });
- item_collection.insert_item(meta_item);
- }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala
index 0b5ed6e4..71ca35f8 100644
--- a/main/src/ui/conversation_summary/message_textview.vala
+++ b/main/src/ui/conversation_summary/message_textview.vala
@@ -24,7 +24,6 @@ public class MessageTextView : TextView {
motion_notify_event.connect(change_cursor_over_url);
update_display_style();
- Util.force_base_background(this, "textview, text:not(:selected)");
style_updated.connect(update_display_style);
populate_popup.connect(populate_context_menu);
}
@@ -60,7 +59,7 @@ public class MessageTextView : TextView {
TextIter end_iter;
buffer.get_iter_at_offset(out start_iter, start);
buffer.get_iter_at_offset(out end_iter, end);
- buffer.apply_tag_by_name("semibold", start_iter, end_iter);
+ buffer.apply_tag(bold_tag, start_iter, end_iter);
}
}
@@ -125,7 +124,7 @@ public class MessageTextView : TextView {
TextIter end_iter;
buffer.get_iter_at_offset(out start_iter, absolute_start + start);
buffer.get_iter_at_offset(out end_iter, absolute_start + end);
- buffer.apply_tag_by_name("url", start_iter, end_iter);
+ buffer.apply_tag(link_tag, start_iter, end_iter);
}
}
diff --git a/main/src/ui/conversation_summary/slashme_message_display.vala b/main/src/ui/conversation_summary/slashme_message_display.vala
deleted file mode 100644
index 1ee20748..00000000
--- a/main/src/ui/conversation_summary/slashme_message_display.vala
+++ /dev/null
@@ -1,79 +0,0 @@
-using Gtk;
-
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class SlashmeMessageDisplay : Plugins.MessageDisplayProvider, Object {
- public string id { get; set; default="slashme"; }
- public double priority { get; set; default=1; }
-
- public StreamInteractor stream_interactor;
-
- public SlashmeMessageDisplay(StreamInteractor stream_interactor) {
- this.stream_interactor = stream_interactor;
- }
-
- public bool can_display(Entities.Message? message) {
- return message.body.has_prefix("/me");
- }
-
- public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
- return new MetaSlashmeItem(stream_interactor, message, conversation);
- }
-}
-
-public class MetaSlashmeItem : Plugins.MetaConversationItem {
- public override Jid? jid { get; set; }
- public override DateTime? sort_time { get; set; }
- public override DateTime? display_time { get; set; }
- public override Encryption? encryption { get; set; }
-
- private StreamInteractor stream_interactor;
- private Conversation conversation;
- private Message message;
- private TextTag nick_tag;
- private MessageTextView text_view;
-
- public MetaSlashmeItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
- this.stream_interactor = stream_interactor;
- this.conversation = conversation;
- this.message = message;
- this.jid = message.from;
- this.sort_time = message.local_time;
- this.seccondary_sort_indicator = message.id + 0.0845;
- this.display_time = message.time;
- this.encryption = message.encryption;
- }
-
- public override bool can_merge { get; set; default=false; }
- public override bool requires_avatar { get; set; default=true; }
- public override bool requires_header { get; set; default=false; }
-
- public override Object? get_widget(Plugins.WidgetType widget_type) {
- text_view = new MessageTextView() { valign=Align.CENTER, vexpand=true, visible = true };
- if (conversation.type_ == Conversation.Type.GROUPCHAT) {
- text_view.highlight_word(conversation.nickname);
- }
-
- string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
- string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view));
- nick_tag = text_view.buffer.create_tag("nick", foreground: "#" + color);
- TextIter iter;
- text_view.buffer.get_start_iter(out iter);
- text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
- text_view.add_text(message.body.substring(3));
-
- text_view.style_updated.connect(update_style);
- text_view.realize.connect(update_style);
- return text_view;
- }
-
- private void update_style() {
- string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view));
- nick_tag.foreground = "#" + color;
- }
-}
-
-}
diff --git a/main/src/ui/conversation_titlebar/search_entry.vala b/main/src/ui/conversation_titlebar/search_entry.vala
new file mode 100644
index 00000000..b452bdce
--- /dev/null
+++ b/main/src/ui/conversation_titlebar/search_entry.vala
@@ -0,0 +1,30 @@
+using Gtk;
+using Gee;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+public class SearchMenuEntry : Plugins.ConversationTitlebarEntry, Object {
+ public string id { get { return "search"; } }
+
+ Plugins.ConversationTitlebarWidget search_button;
+
+ public SearchMenuEntry(Plugins.ConversationTitlebarWidget search_button) {
+ this.search_button = search_button;
+ }
+
+ public double order { get { return 1; } }
+ public Plugins.ConversationTitlebarWidget? get_widget(Plugins.WidgetType type) {
+ if (type == Plugins.WidgetType.GTK) {
+ return search_button;
+ }
+ return null;
+ }
+}
+
+public class GlobalSearchButton : Plugins.ConversationTitlebarWidget, Gtk.ToggleButton {
+ public new void set_conversation(Conversation conversation) { }
+}
+
+}
diff --git a/main/src/ui/conversation_titlebar/view.vala b/main/src/ui/conversation_titlebar/view.vala
index d01cd9bb..13a9bf80 100644
--- a/main/src/ui/conversation_titlebar/view.vala
+++ b/main/src/ui/conversation_titlebar/view.vala
@@ -11,6 +11,7 @@ public class ConversationTitlebar : Gtk.HeaderBar {
private Window window;
private Conversation? conversation;
private Gee.List<Plugins.ConversationTitlebarWidget> widgets = new ArrayList<Plugins.ConversationTitlebarWidget>();
+ public GlobalSearchButton search_button = new GlobalSearchButton() { visible = true };
public ConversationTitlebar(StreamInteractor stream_interactor, Window window) {
this.stream_interactor = stream_interactor;
@@ -19,9 +20,11 @@ public class ConversationTitlebar : Gtk.HeaderBar {
this.get_style_context().add_class("dino-right");
show_close_button = true;
hexpand = true;
+ search_button.set_image(new Gtk.Image.from_icon_name("system-search-symbolic", Gtk.IconSize.MENU) { visible = true });
Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor));
+ app.plugin_registry.register_contact_titlebar_entry(new SearchMenuEntry(search_button));
app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window));
foreach(var e in app.plugin_registry.conversation_titlebar_entries) {
diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala
new file mode 100644
index 00000000..eadf142c
--- /dev/null
+++ b/main/src/ui/global_search.vala
@@ -0,0 +1,259 @@
+using Gee;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")]
+class GlobalSearch : Overlay {
+ public signal void selected_item(MessageItem item);
+ private StreamInteractor stream_interactor;
+ private string search = "";
+ private int loaded_results = -1;
+ private Mutex reloading_mutex = Mutex();
+
+ [GtkChild] public SearchEntry search_entry;
+ [GtkChild] public Label entry_number_label;
+ [GtkChild] public ScrolledWindow results_scrolled;
+ [GtkChild] public Box results_box;
+ [GtkChild] public Stack results_empty_stack;
+ [GtkChild] public Frame auto_complete_overlay;
+ [GtkChild] public ListBox auto_complete_list;
+
+ public GlobalSearch init(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ search_entry.search_changed.connect(() => {
+ set_search(search_entry.text);
+ });
+ search_entry.notify["text"].connect_after(() => { update_auto_complete(); });
+ search_entry.notify["cursor-position"].connect_after(() => { update_auto_complete(); });
+
+ results_scrolled.vadjustment.notify["value"].connect(() => {
+ if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) {
+ if (!reloading_mutex.trylock()) return;
+ Gee.List<MessageItem> new_messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search, loaded_results);
+ if (new_messages.size == 0) {
+ reloading_mutex.unlock();
+ return;
+ }
+ loaded_results += new_messages.size;
+ append_messages(new_messages);
+ }
+ });
+ results_scrolled.vadjustment.notify["upper"].connect_after(() => {
+ reloading_mutex.trylock();
+ reloading_mutex.unlock();
+ });
+
+ event.connect((event) => {
+ if (auto_complete_overlay.visible) {
+ if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Up) {
+ var row = auto_complete_list.get_selected_row();
+ var index = row == null ? -1 : row.get_index() - 1;
+ if (index == -1) index = (int)auto_complete_list.get_children().length() - 1;
+ auto_complete_list.select_row(auto_complete_list.get_row_at_index(index));
+ return true;
+ }
+ if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Down) {
+ var row = auto_complete_list.get_selected_row();
+ var index = row == null ? 0 : row.get_index() + 1;
+ if (index == auto_complete_list.get_children().length()) index = 0;
+ auto_complete_list.select_row(auto_complete_list.get_row_at_index(index));
+ return true;
+ }
+ if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Tab ||
+ event.type == Gdk.EventType.KEY_RELEASE && event.key.keyval == Gdk.Key.Return) {
+ auto_complete_list.get_selected_row().activate();
+ return true;
+ }
+ }
+ // TODO: Handle cursor movement in results
+ // TODO: Direct all keystrokes to text input
+ return false;
+ });
+
+ return this;
+ }
+
+ private void update_auto_complete() {
+ Gee.List<SearchSuggestion> suggestions = stream_interactor.get_module(SearchProcessor.IDENTITY).suggest_auto_complete(search_entry.text, search_entry.cursor_position);
+ auto_complete_overlay.visible = suggestions.size > 0;
+ if (suggestions.size > 0) {
+ auto_complete_list.@foreach((widget) => auto_complete_list.remove(widget));
+ foreach(SearchSuggestion suggestion in suggestions) {
+ Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui");
+ AvatarImage avatar = (AvatarImage)builder.get_object("image");
+ avatar.set_jid(stream_interactor, suggestion.jid, suggestion.account);
+ Label label = (Label)builder.get_object("label");
+ string display_name = Util.get_display_name(stream_interactor, suggestion.jid, suggestion.account);
+ if (display_name != suggestion.jid.to_string()) {
+ label.set_markup(@"$display_name <span font_weight='light' fgalpha='80%'>$(suggestion.jid)</span>");
+ } else {
+ label.label = display_name;
+ }
+ ListBoxRow row = new ListBoxRow() { visible = true, can_focus = false };
+ row.add((Widget)builder.get_object("root"));
+ row.activate.connect(() => {
+ handle_suggestion(suggestion);
+ });
+ auto_complete_list.add(row);
+ }
+ auto_complete_list.select_row(auto_complete_list.get_row_at_index(0));
+ }
+ }
+
+ private void handle_suggestion(SearchSuggestion suggestion) {
+ search_entry.move_cursor(MovementStep.LOGICAL_POSITIONS, suggestion.start_index - search_entry.cursor_position, false);
+ search_entry.delete_from_cursor(DeleteType.CHARS, suggestion.end_index - suggestion.start_index);
+ search_entry.insert_at_cursor(suggestion.completion + " ");
+ }
+
+ private void clear_search() {
+ results_box.@foreach((widget) => { widget.destroy(); });
+ }
+
+ private void set_search(string search) {
+ clear_search();
+ this.search = search;
+
+ if (get_keywords(search).is_empty) {
+ results_empty_stack.set_visible_child_name("empty");
+ return;
+ }
+
+ Gee.List<MessageItem> messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search);
+ if (messages.size == 0) {
+ results_empty_stack.set_visible_child_name("no-result");
+ } else {
+ results_empty_stack.set_visible_child_name("results");
+
+ int match_count = messages.size < 10 ? messages.size : stream_interactor.get_module(SearchProcessor.IDENTITY).count_match_messages(search);
+ entry_number_label.label = "<i>" + _("%i search results").printf(match_count) + "</i>";
+ loaded_results += messages.size;
+ append_messages(messages);
+ }
+ }
+
+ private void append_messages(Gee.List<MessageItem> messages) {
+ foreach (MessageItem item in messages) {
+ Gee.List<MessageItem> before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(item.conversation, item.message.local_time, item.message.id, 1);
+ Gee.List<MessageItem> after_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(item.conversation, item.message.local_time, item.message.id, 1);
+
+ Box context_box = new Box(Orientation.VERTICAL, 5) { visible=true };
+ if (before_message != null && before_message.size > 0) {
+ context_box.add(get_context_message_widget(before_message.first()));
+ }
+
+ Widget match_widget = get_match_message_widget(item);
+ Util.force_alloc_width(match_widget, results_empty_stack.get_allocated_width() - results_box.margin * 2);
+ context_box.add(match_widget);
+
+ if (after_message != null && after_message.size > 0) {
+ context_box.add(get_context_message_widget(after_message.first()));
+ }
+
+ Label date_label = new Label(ConversationSummary.DefaultSkeletonHeader.get_relative_time(item.display_time)) { xalign=0, visible=true };
+ date_label.get_style_context().add_class("dim-label");
+
+ string display_name = Util.get_conversation_display_name(stream_interactor, item.conversation);
+ string title = item.message.type_ == Message.Type.GROUPCHAT ? _("In %s").printf(display_name) : _("With %s").printf(display_name);
+ Box header_box = new Box(Orientation.HORIZONTAL, 10) { margin_left=7, visible=true };
+ header_box.add(new Label(@"<b>$(Markup.escape_text(title))</b>") { ellipsize=EllipsizeMode.END, xalign=0, use_markup=true, visible=true });
+ header_box.add(date_label);
+
+ Box result_box = new Box(Orientation.VERTICAL, 7) { visible=true };
+ result_box.add(header_box);
+ result_box.add(context_box);
+
+ results_box.add(result_box);
+ }
+ }
+
+ private Widget get_match_message_widget(MessageItem item) {
+ Grid grid = get_skeleton(item);
+ grid.margin_top = 3;
+ grid.margin_bottom = 3;
+
+ string text = item.message.body.replace("\n", "").replace("\r", "");
+ if (text.length > 200) {
+ int index = text.index_of(search);
+ if (index + search.length <= 100) {
+ text = text.substring(0, 150) + " … " + text.substring(text.length - 50, 50);
+ } else if (index >= text.length - 100) {
+ text = text.substring(0, 50) + " … " + text.substring(text.length - 150, 150);
+ } else {
+ text = text.substring(0, 25) + " … " + text.substring(index - 50, 50) + text.substring(index, 100) + " … " + text.substring(text.length - 25, 25);
+ }
+ }
+ TextView tv = new TextView() { wrap_mode=Gtk.WrapMode.WORD_CHAR, hexpand=true, visible=true };
+ tv.buffer.text = text;
+ TextTag link_tag = tv.buffer.create_tag("hit", background: "yellow");
+
+ Gee.List<string> keywords = get_keywords(Regex.escape_string(search.down()));
+ foreach (string keyword in keywords) {
+ Regex url_regex = new Regex(keyword.down());
+ MatchInfo match_info;
+ url_regex.match(text.down(), 0, out match_info);
+ for (; match_info.matches(); match_info.next()) {
+ int start;
+ int end;
+ match_info.fetch_pos(0, out start, out end);
+ start = text[0:start].char_count();
+ end = text[0:end].char_count();
+ TextIter start_iter;
+ TextIter end_iter;
+ tv.buffer.get_iter_at_offset(out start_iter, start);
+ tv.buffer.get_iter_at_offset(out end_iter, end);
+ tv.buffer.apply_tag(link_tag, start_iter, end_iter);
+ }
+ }
+
+ grid.attach(tv, 1, 1, 1, 1);
+
+ Button button = new Button() { relief=ReliefStyle.NONE, visible=true };
+ button.clicked.connect(() => {
+ selected_item(item);
+ });
+ button.add(grid);
+ return button;
+ }
+
+ private Grid get_context_message_widget(MessageItem item) {
+ Grid grid = get_skeleton(item);
+ grid.margin_left = 7;
+ Label label = new Label(item.message.body.replace("\n", "").replace("\r", "")) { ellipsize=EllipsizeMode.MIDDLE, xalign=0, visible=true };
+ grid.attach(label, 1, 1, 1, 1);
+ grid.opacity = 0.55;
+ return grid;
+ }
+
+ private Grid get_skeleton(MessageItem item) {
+ AvatarImage image = new AvatarImage() { height=32, width=32, margin_right=7, valign=Align.START, visible=true, allow_gray = false };
+ image.set_jid(stream_interactor, item.jid, item.message.account);
+ Grid grid = new Grid() { row_homogeneous=false, visible=true };
+ grid.attach(image, 0, 0, 1, 2);
+
+ string display_name = Util.get_display_name(stream_interactor, item.jid, item.message.account);
+ string color = Util.get_name_hex_color(stream_interactor, item.message.account, item.jid, false); // TODO Util.is_dark_theme(name_label)
+ Label name_label = new Label("") { use_markup=true, xalign=0, visible=true };
+ name_label.label = @"<span size='small' foreground=\"#$color\">$display_name</span>";
+ grid.attach(name_label, 1, 0, 1, 1);
+ return grid;
+ }
+
+ private static Gee.List<string> get_keywords(string search_string) {
+ Gee.List<string> ret = new ArrayList<string>();
+ foreach (string search in search_string.split(" ")) {
+ bool is_filter = search.has_prefix("from:") || search.has_prefix("in:") || search.has_prefix("with:");
+ if (!is_filter && search != "") {
+ ret.add(search);
+ }
+ }
+ return ret;
+ }
+}
+
+}
diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala
index e2798def..61a22085 100644
--- a/main/src/ui/unified_window.vala
+++ b/main/src/ui/unified_window.vala
@@ -1,11 +1,12 @@
using Gee;
+using Gdk;
using Gtk;
using Dino.Entities;
namespace Dino.Ui {
-public class UnifiedWindow : Window {
+public class UnifiedWindow : Gtk.Window {
private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true };
private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true };
@@ -16,7 +17,12 @@ public class UnifiedWindow : Window {
private ConversationTitlebar conversation_titlebar;
private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true };
private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true };
- private Paned paned = new Paned(Orientation.HORIZONTAL) { visible=true };
+ private Paned paned;
+ private Revealer goto_end_revealer;
+ private Button goto_end_button;
+ private Revealer search_revealer;
+ private SearchEntry search_entry;
+ private GlobalSearch search_box;
private Stack stack = new Stack() { visible=true };
private StreamInteractor stream_interactor;
@@ -36,8 +42,47 @@ public class UnifiedWindow : Window {
setup_unified();
setup_stack();
- conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_revealer, "reveal-child",
- BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+ var vadjustment = conversation_frame.scrolled.vadjustment;
+ vadjustment.notify["value"].connect(() => {
+ goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size;
+ });
+ goto_end_button.clicked.connect(() => {
+ conversation_frame.initialize_for_conversation(conversation);
+ });
+
+ conversation_titlebar.search_button.clicked.connect(() => {
+ search_revealer.reveal_child = conversation_titlebar.search_button.active;
+ });
+ search_revealer.notify["child-revealed"].connect(() => {
+ if (search_revealer.child_revealed) {
+ if (conversation_frame.conversation != null && search_box.search_entry.text == "") {
+ reset_search_entry();
+ }
+ search_box.search_entry.grab_focus();
+ }
+ });
+ search_box.selected_item.connect((item) => {
+ on_conversation_selected(item.conversation, false, false);
+ conversation_frame.initialize_around_message(item.conversation, item);
+ close_search();
+ });
+ event.connect((event) => {
+ if (event.type == EventType.BUTTON_PRESS) {
+ int dest_x, dest_y;
+ bool ret = search_box.translate_coordinates(this, 0, 0, out dest_x, out dest_y);
+ int geometry_x, geometry_y, geometry_width, geometry_height;
+ this.get_window().get_geometry(out geometry_x, out geometry_y, out geometry_width, out geometry_height);
+ if (ret && event.button.x_root - geometry_x < dest_x || event.button.y_root - geometry_y < dest_y) {
+ close_search();
+ }
+ } else if (event.type == EventType.KEY_RELEASE) {
+ if (event.key.keyval == Gdk.Key.Escape) {
+ close_search();
+ }
+ }
+ return false;
+ });
+
paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
focus_in_event.connect(on_focus_in_event);
@@ -50,38 +95,60 @@ public class UnifiedWindow : Window {
accounts_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("accounts", null); });
conversations_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("add_chat", null); });
conversations_placeholder.secondary_button.clicked.connect(() => { get_application().activate_action("add_conference", null); });
- filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected);
- conversation_list_titlebar.conversation_opened.connect(on_conversation_selected);
+ filterable_conversation_list.conversation_list.conversation_selected.connect((conversation) => on_conversation_selected(conversation));
+ conversation_list_titlebar.conversation_opened.connect((conversation) => on_conversation_selected(conversation));
check_stack();
}
- public void on_conversation_selected(Conversation conversation) {
+ private void reset_search_entry() {
+ if (conversation_frame.conversation != null) {
+ switch (conversation.type_) {
+ case Conversation.Type.CHAT:
+ case Conversation.Type.GROUPCHAT_PM:
+ search_box.search_entry.text = @"with:$(conversation.counterpart) ";
+ break;
+ case Conversation.Type.GROUPCHAT:
+ search_box.search_entry.text = @"in:$(conversation.counterpart) ";
+ break;
+ }
+ }
+ }
+
+ public void on_conversation_selected(Conversation conversation, bool do_reset_search = true, bool default_initialize_conversation = true) {
if (this.conversation == null || !this.conversation.equals(conversation)) {
this.conversation = conversation;
stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation);
conversation.active = true; // only for conversation_selected
filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened
+ if (do_reset_search) {
+ reset_search_entry();
+ }
chat_input.initialize_for_conversation(conversation);
- conversation_frame.initialize_for_conversation(conversation);
+ if (default_initialize_conversation) {
+ conversation_frame.initialize_for_conversation(conversation);
+ }
conversation_titlebar.initialize_for_conversation(conversation);
}
}
+ private void close_search() {
+ conversation_titlebar.search_button.active = false;
+ search_revealer.reveal_child = false;
+ }
+
private void setup_unified() {
- chat_input = new ChatInput.View(stream_interactor) { visible=true };
- conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true };
- filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true };
-
- Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true };
- grid.get_style_context().add_class("dino-conversation");
- grid.add(conversation_frame);
- grid.add(chat_input);
-
- paned.set_position(300);
- paned.pack1(filterable_conversation_list, false, false);
- paned.pack2(grid, true, false);
+ Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui");
+ paned = (Paned) builder.get_object("paned");
+ chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor);
+ conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).init(stream_interactor);
+ filterable_conversation_list = ((ConversationSelector.View) builder.get_object("conversation_list")).init(stream_interactor);
+ goto_end_revealer = (Revealer) builder.get_object("goto_end_revealer");
+ goto_end_button = (Button) builder.get_object("goto_end_button");
+ search_box = ((GlobalSearch) builder.get_object("search_box")).init(stream_interactor);
+ search_revealer = (Revealer) builder.get_object("search_revealer");
+ search_entry = (SearchEntry) builder.get_object("search_entry");
}
private void setup_headerbar() {
diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala
index 3cadfffb..4e9e942d 100644
--- a/main/src/ui/util/helper.vala
+++ b/main/src/ui/util/helper.vala
@@ -118,10 +118,6 @@ public static void force_background(Gtk.Widget widget, string color, string sele
force_css(widget, force_background_css.printf(selector, color));
}
-public static void force_base_background(Gtk.Widget widget, string selector = "*") {
- force_background(widget, "@theme_base_color", selector);
-}
-
public static void force_color(Gtk.Widget widget, string color, string selector = "*") {
force_css(widget, force_color_css.printf(selector, color));
}
@@ -142,4 +138,13 @@ public static bool is_24h_format() {
return settings_format == "24h" || p_format == " ";
}
+// Workaround GTK TextView issues
+public static void force_alloc_width(Widget widget, int width) {
+ Allocation alloc = Allocation();
+ widget.get_preferred_width(out alloc.width, null);
+ widget.get_preferred_height(out alloc.height, null);
+ alloc.width = width;
+ widget.size_allocate(alloc);
+}
+
}
diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala
index b647cdbb..7616d535 100644
--- a/plugins/http-files/src/file_provider.vala
+++ b/plugins/http-files/src/file_provider.vala
@@ -90,7 +90,7 @@ public class FileProvider : Dino.FileProvider, Object {
file_transfer.state = FileTransfer.State.NOT_STARTED;
file_transfer.provider = 0;
file_transfer.info = message.id.to_string();
- file_incoming(file_transfer);
+ file_incoming(file_transfer, conversation);
success = true;
Idle.add((owned)callback);
});
diff --git a/plugins/http-files/src/manager.vala b/plugins/http-files/src/manager.vala
index 7335b89a..9faa8933 100644
--- a/plugins/http-files/src/manager.vala
+++ b/plugins/http-files/src/manager.vala
@@ -81,28 +81,25 @@ 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;
}
}
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;
}
diff --git a/plugins/http-files/src/plugin.vala b/plugins/http-files/src/plugin.vala
index 1fc0c9fd..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.plugin_registry.register_message_display(new FileMessageFilterDisplay(app.db));
+ app.stream_interactor.get_module(ContentItemStore.IDENTITY).add_filter(new FileMessageFilter(app.db));
}
public void shutdown() {
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<T> {
+ 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<T> {
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<T> {
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<T> {
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<T> {
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<T> {
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<T> {
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<T> {
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<T> {
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/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..88f05e04 100644
--- a/qlite/src/query_builder.vala
+++ b/qlite/src/query_builder.vala
@@ -10,16 +10,22 @@ 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 = {};
+ // GROUP BY [...]
+ private string? group_by_term;
+
// LIMIT [...] OFFSET [...]
private int limit_val;
private int offset_val;
@@ -30,12 +36,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 {
@@ -50,21 +56,45 @@ 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 outer_join_with<T>(Table table, Column<T> lhs, Column<T> 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<T>(Table table, Column<T> lhs, Column<T> 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);
}
@@ -74,17 +104,17 @@ public class QueryBuilder : StatementBuilder {
public QueryBuilder with<T>(Column<T> column, string comp, T value) {
if ((column.unique || column.primary_key) && comp == "=") single_result = true;
selection_args += new Field<T>(column, value);
- selection = @"($selection) AND $(column.name) $comp ?";
+ selection = @"($selection) AND $column $comp ?";
return this;
}
public QueryBuilder with_null<T>(Column<T> column) {
- selection = @"($selection) AND $(column.name) ISNULL";
+ selection = @"($selection) AND $column ISNULL";
return this;
}
public QueryBuilder without_null<T>(Column<T> column) {
- selection = @"($selection) AND $(column.name) NOT NULL";
+ selection = @"($selection) AND $column NOT NULL";
return this;
}
@@ -98,6 +128,17 @@ public class QueryBuilder : StatementBuilder {
return this;
}
+ public QueryBuilder group_by(Column[] columns) {
+ foreach(Column col in columns) {
+ if (group_by_term == null) {
+ group_by_term = col.to_string();
+ } else {
+ group_by_term += @", $col";
+ }
+ }
+ return this;
+ }
+
public QueryBuilder limit(int limit) {
if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit");
this.limit_val = limit;
@@ -135,7 +176,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 $(group_by_term == null ? "" : @"GROUP BY $group_by_term") $(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);
}
@@ -147,13 +188,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;
}
@@ -177,4 +218,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_name(@"_fts_$table_name", @"_fts_$table_name.docid = $table_name.rowid");
+ }
+
+ public MatchQueryBuilder match(Column<string> 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/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, string? table = null) {
+ return int_map.has_key(field_name(field, table));
}
- public bool has_integer(string field) {
- return int_map.has_key(field);
+ public double get_real(string field, string? table = null, double def = 0) {
+ return real_map[field_name(field, table)] ?? def;
}
- public double get_real(string field, double def = 0) {
- return real_map[field] ?? def;
+ 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 bool has_real(string field) {
- return real_map.has_key(field) && real_map[field] != 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 00b4ef00..607a396c 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;
@@ -17,6 +19,37 @@ 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) {
+ 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.to_column_definition())";
+ 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) {
@@ -37,6 +70,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 +95,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<string> column, string query) {
+ return match_query().match(column, query);
+ }
+
public InsertBuilder insert() {
ensure_init();
return db.insert().into(this);
@@ -98,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)";
@@ -107,6 +153,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) {
@@ -114,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");
}