Merge pull request #415 from bobufa/message-search

Message search
This commit is contained in:
fiaxh 2018-08-31 16:25:51 +02:00 committed by GitHub
commit ecb18afdb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2146 additions and 824 deletions

View file

@ -29,6 +29,7 @@ SOURCES
src/service/blocking_manager.vala src/service/blocking_manager.vala
src/service/chat_interaction.vala src/service/chat_interaction.vala
src/service/connection_manager.vala src/service/connection_manager.vala
src/service/content_item_store.vala
src/service/conversation_manager.vala src/service/conversation_manager.vala
src/service/counterpart_interaction_manager.vala src/service/counterpart_interaction_manager.vala
src/service/database.vala src/service/database.vala
@ -42,6 +43,7 @@ SOURCES
src/service/presence_manager.vala src/service/presence_manager.vala
src/service/registration.vala src/service/registration.vala
src/service/roster_manager.vala src/service/roster_manager.vala
src/service/search_processor.vala
src/service/stream_interactor.vala src/service/stream_interactor.vala
src/service/util.vala src/service/util.vala

View file

@ -38,6 +38,8 @@ public interface Dino.Application : GLib.Application {
ChatInteraction.start(stream_interactor); ChatInteraction.start(stream_interactor);
FileManager.start(stream_interactor, db); FileManager.start(stream_interactor, db);
NotificationEvents.start(stream_interactor); NotificationEvents.start(stream_interactor);
ContentItemStore.start(stream_interactor, db);
SearchProcessor.start(stream_interactor, db);
create_actions(); create_actions();

View file

@ -23,7 +23,21 @@ public class FileTransfer : Object {
public DateTime? local_time { get; set; } public DateTime? local_time { get; set; }
public Encryption encryption { 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 OutputStream output_stream { get; set; }
public string file_name { get; set; } public string file_name { get; set; }
@ -41,9 +55,11 @@ public class FileTransfer : Object {
public string info { get; set; } public string info { get; set; }
private Database? db; 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.db = db;
this.storage_dir = storage_dir;
id = row[db.file_transfer.id]; id = row[db.file_transfer.id];
account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO dont have to generate acc new account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO dont have to generate acc new
@ -61,7 +77,7 @@ public class FileTransfer : Object {
} }
direction = row[db.file_transfer.direction]; direction = row[db.file_transfer.direction];
time = new DateTime.from_unix_utc(row[db.file_transfer.time]); 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]; encryption = (Encryption) row[db.file_transfer.encryption];
file_name = row[db.file_transfer.file_name]; file_name = row[db.file_transfer.file_name];
path = row[db.file_transfer.path]; path = row[db.file_transfer.path];

View file

@ -82,7 +82,7 @@ public class Message : Object {
} }
direction = row[db.message.direction]; direction = row[db.message.direction];
time = new DateTime.from_unix_utc(row[db.message.time]); 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]; body = row[db.message.body];
marked = (Message.Marked) row[db.message.marked]; marked = (Message.Marked) row[db.message.marked];
encryption = (Encryption) row[db.message.encryption]; encryption = (Encryption) row[db.message.encryption];

View file

@ -75,15 +75,16 @@ public interface ConversationTitlebarWidget : Object {
public abstract interface ConversationItemPopulator : Object { public abstract interface ConversationItemPopulator : Object {
public abstract string id { get; } public abstract string id { get; }
public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type); 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 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 abstract class MetaConversationItem : Object {
public virtual string populator_id { get; set; }
public virtual Jid? jid { get; set; default=null; } 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 bool dim { get; set; default=false; }
public virtual DateTime? sort_time { get; set; default=null; } public virtual DateTime? sort_time { get; set; default=null; }
public virtual double seccondary_sort_indicator { get; set; } public virtual double seccondary_sort_indicator { get; set; }
@ -103,21 +104,4 @@ public interface ConversationItemCollection : Object {
public signal void remove_item(MetaConversationItem item); 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);
}
} }

View file

@ -7,8 +7,7 @@ public class Registry {
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>(); internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>(); internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>(); internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
internal Gee.List<MessageDisplayProvider> message_displays = new ArrayList<MessageDisplayProvider>(); internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
internal Gee.List<ConversationItemPopulator> conversation_item_populators = new ArrayList<ConversationItemPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => { internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
if (a.order < b.order) { if (a.order < b.order) {
return -1; return -1;
@ -70,22 +69,12 @@ public class Registry {
} }
} }
public bool register_message_display(MessageDisplayProvider provider) { public bool register_conversation_addition_populator(ConversationAdditionPopulator populator) {
lock (message_displays) { lock (conversation_addition_populators) {
foreach(MessageDisplayProvider p in message_displays) { foreach(ConversationItemPopulator p in conversation_addition_populators) {
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) {
if (p.id == populator.id) return false; if (p.id == populator.id) return false;
} }
conversation_item_populators.add(populator); conversation_addition_populators.add(populator);
return true; return true;
} }
} }

View file

@ -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();
}
}
}

View file

@ -9,7 +9,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } } public string id { get { return IDENTITY.id; } }
public signal void received_state(Account account, Jid jid, string state); 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_received(Account account, Jid jid, Entities.Message message);
public signal void received_message_displayed(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; 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); Conversation? conversation = stream_interactor.get_module(MessageStorage.IDENTITY).get_conversation_for_stanza_id(account, stanza_id);
if (conversation == null) return; 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; if (message == null) return;
conversation.read_up_to = message; conversation.read_up_to = message;
} else { } else {
foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_conversations(jid, account)) { 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) { if (message != null) {
switch (marker) { switch (marker) {
case Xep.ChatMarkers.MARKER_RECEIVED: case Xep.ChatMarkers.MARKER_RECEIVED:

View file

@ -6,7 +6,7 @@ using Dino.Entities;
namespace Dino { namespace Dino {
public class Database : Qlite.Database { public class Database : Qlite.Database {
private const int VERSION = 6; private const int VERSION = 8;
public class AccountTable : Table { public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; 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 class MessageTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<string> stanza_id = new Column.Text("stanza_id"); 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, init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
type_, time, local_time, body, encryption, marked}); type_, time, local_time, body, encryption, marked});
index("message_localtime_counterpart_idx", {local_time, counterpart_id}); 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 AccountTable account { get; private set; }
public JidTable jid { get; private set; } public JidTable jid { get; private set; }
public ContentTable content { get; private set; }
public MessageTable message { get; private set; } public MessageTable message { get; private set; }
public RealJidTable real_jid { get; private set; } public RealJidTable real_jid { get; private set; }
public FileTransferTable file_transfer { get; private set; } public FileTransferTable file_transfer { get; private set; }
@ -190,6 +207,7 @@ public class Database : Qlite.Database {
base(fileName, VERSION); base(fileName, VERSION);
account = new AccountTable(this); account = new AccountTable(this);
jid = new JidTable(this); jid = new JidTable(this);
content = new ContentTable(this);
message = new MessageTable(this); message = new MessageTable(this);
real_jid = new RealJidTable(this); real_jid = new RealJidTable(this);
file_transfer = new FileTransferTable(this); file_transfer = new FileTransferTable(this);
@ -198,7 +216,7 @@ public class Database : Qlite.Database {
entity_feature = new EntityFeatureTable(this); entity_feature = new EntityFeatureTable(this);
roster = new RosterTable(this); roster = new RosterTable(this);
settings = new SettingsTable(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 { try {
exec("PRAGMA synchronous=0"); exec("PRAGMA synchronous=0");
} catch (Error e) { } } catch (Error e) { }
@ -206,6 +224,31 @@ public class Database : Qlite.Database {
public override void migrate(long oldVersion) { public override void migrate(long oldVersion) {
// new table columns are added, outdated columns are still present // 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() { 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) { public int add_content_item(Conversation conversation, DateTime time, DateTime local_time, int content_type, int foreign_id) {
QueryBuilder select = message.select() return (int) content.insert()
.with(message.counterpart_id, "=", get_jid_id(jid)) .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) .with(message.account_id, "=", account.id)
.order_by(message.id, "DESC")
.limit(count); .limit(count);
if (jid.resourcepart != null) { if (jid.resourcepart != null) {
select.with(message.counterpart_resource, "=", jid.resourcepart); select.with(message.counterpart_resource, "=", jid.resourcepart);
@ -244,9 +317,6 @@ public class Database : Qlite.Database {
if (type != null) { if (type != null) {
select.with(message.type_, "=", (int) type); select.with(message.type_, "=", (int) type);
} }
if (before != null) {
select.with(message.local_time, "<", (long) before.to_unix());
}
LinkedList<Message> ret = new LinkedList<Message>(); LinkedList<Message> ret = new LinkedList<Message>();
foreach (Row row in select) { foreach (Row row in select) {

View file

@ -11,7 +11,7 @@ public class FileManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } } public string id { get { return IDENTITY.id; } }
public signal void upload_available(Account account); 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 StreamInteractor stream_interactor;
private Database db; private Database db;
@ -68,7 +68,7 @@ public class FileManager : StreamInteractionModule, Object {
file_sender.send_file(conversation, file_transfer); file_sender.send_file(conversation, file_transfer);
} }
} }
received_file(file_transfer); received_file(file_transfer, conversation);
} }
public bool is_upload_available(Conversation conversation) { public bool is_upload_available(Conversation conversation) {
@ -78,21 +78,38 @@ public class FileManager : StreamInteractionModule, Object {
return false; 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)before.to_unix())
.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() Qlite.QueryBuilder select = db.file_transfer.select()
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
.with(db.file_transfer.account_id, "=", account.id) .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)after.to_unix())
.with(db.file_transfer.local_time, "<", (long)before.to_unix()) .limit(n);
.order_by(db.file_transfer.id, "DESC"); return get_transfers_from_qry(select);
}
private Gee.List<FileTransfer> get_transfers_from_qry(Qlite.QueryBuilder select) {
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>(); Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
foreach (Qlite.Row row in select) { foreach (Qlite.Row row in select) {
FileTransfer file_transfer = new FileTransfer.from_row(db, row); FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());
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) { }
ret.insert(0, file_transfer); ret.insert(0, file_transfer);
} }
return ret; return ret;
@ -117,7 +134,7 @@ public class FileManager : StreamInteractionModule, Object {
outgoing_processors.add(processor); 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) { foreach (IncommingFileProcessor processor in incomming_processors) {
if (processor.can_process(file_transfer)) { if (processor.can_process(file_transfer)) {
processor.process(file_transfer); processor.process(file_transfer);
@ -131,7 +148,7 @@ public class FileManager : StreamInteractionModule, Object {
} catch (Error e) { } } catch (Error e) { }
file_transfer.persist(db); file_transfer.persist(db);
received_file(file_transfer); received_file(file_transfer, conversation);
} }
private void save_file(FileTransfer file_transfer) { private void save_file(FileTransfer file_transfer) {
@ -152,7 +169,7 @@ public class FileManager : StreamInteractionModule, Object {
} }
public interface FileProvider : 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 { public interface FileSender : Object {

View file

@ -1,4 +1,5 @@
using Gee; using Gee;
using Qlite;
using Dino.Entities; using Dino.Entities;
@ -51,26 +52,47 @@ public class MessageStorage : StreamInteractionModule, Object {
return null; return null;
} }
public Gee.List<Message>? get_messages_before_message(Conversation? conversation, Message message, int count = 20) { 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); // SortedSet<Message>? before = messages[conversation].head_set(message);
if (before != null && before.size >= count) { // if (before != null && before.size >= count) {
Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func); // Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
Iterator<Message> iter = before.iterator(); // Iterator<Message> iter = before.iterator();
iter.next(); // iter.next();
for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next(); // for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next();
while(iter.has_next()) { // while(iter.has_next()) {
Message m = iter.get(); // Message m = iter.get();
ret.add(m); // ret.add(m);
iter.next(); // 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; 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 Message? get_message_by_id(string stanza_id, Conversation conversation) { 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_stanza_id(string stanza_id, Conversation conversation) {
init_conversation(conversation); init_conversation(conversation);
foreach (Message message in messages[conversation]) { foreach (Message message in messages[conversation]) {
if (message.stanza_id == stanza_id) return message; if (message.stanza_id == stanza_id) return message;
@ -100,7 +122,7 @@ public class MessageStorage : StreamInteractionModule, Object {
} }
return res; 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); messages[conversation].add_all(db_messages);
} }
} }

View file

@ -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;
}
}
}

View file

@ -29,6 +29,7 @@ set(RESOURCE_LIST
chat_input.ui chat_input.ui
contact_details_dialog.ui contact_details_dialog.ui
conversation_list_titlebar.ui conversation_list_titlebar.ui
global_search.ui
conversation_selector/view.ui conversation_selector/view.ui
conversation_selector/chat_row_tooltip.ui conversation_selector/chat_row_tooltip.ui
conversation_selector/conversation_row.ui conversation_selector/conversation_row.ui
@ -43,7 +44,9 @@ set(RESOURCE_LIST
menu_encryption.ui menu_encryption.ui
occupant_list.ui occupant_list.ui
occupant_list_item.ui occupant_list_item.ui
search_autocomplete.ui
settings_dialog.ui settings_dialog.ui
unified_main_content.ui
unified_window_placeholder.ui unified_window_placeholder.ui
theme.css theme.css
@ -93,6 +96,7 @@ SOURCES
src/ui/contact_details/dialog.vala src/ui/contact_details/dialog.vala
src/ui/contact_details/muc_config_form_provider.vala src/ui/contact_details/muc_config_form_provider.vala
src/ui/conversation_list_titlebar.vala src/ui/conversation_list_titlebar.vala
src/ui/global_search.vala
src/ui/conversation_selector/chat_row.vala src/ui/conversation_selector/chat_row.vala
src/ui/conversation_selector/conversation_row.vala src/ui/conversation_selector/conversation_row.vala
src/ui/conversation_selector/groupchat_pm_row.vala src/ui/conversation_selector/groupchat_pm_row.vala
@ -100,19 +104,16 @@ SOURCES
src/ui/conversation_selector/list.vala src/ui/conversation_selector/list.vala
src/ui/conversation_selector/view.vala src/ui/conversation_selector/view.vala
src/ui/conversation_summary/chat_state_populator.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_item_skeleton.vala
src/ui/conversation_summary/conversation_view.vala src/ui/conversation_summary/conversation_view.vala
src/ui/conversation_summary/date_separator_populator.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/message_textview.vala
src/ui/conversation_summary/slashme_message_display.vala
src/ui/conversation_summary/subscription_notification.vala src/ui/conversation_summary/subscription_notification.vala
src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/menu_entry.vala
src/ui/conversation_titlebar/occupants_entry.vala src/ui/conversation_titlebar/occupants_entry.vala
src/ui/conversation_titlebar/search_entry.vala
src/ui/conversation_titlebar/view.vala src/ui/conversation_titlebar/view.vala
src/ui/manage_accounts/account_row.vala src/ui/manage_accounts/account_row.vala
src/ui/manage_accounts/add_account_dialog.vala src/ui/manage_accounts/add_account_dialog.vala

View file

@ -22,20 +22,5 @@
<property name="pack_type">start</property> <property name="pack_type">start</property>
</packing> </packing>
</child> </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> </template>
</interface> </interface>

View file

@ -4,21 +4,6 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="expand">True</property> <property name="expand">True</property>
<property name="orientation">vertical</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> <child>
<object class="GtkScrolledWindow" id="scrolled"> <object class="GtkScrolledWindow" id="scrolled">
<property name="expand">True</property> <property name="expand">True</property>

169
main/data/global_search.ui Normal file
View file

@ -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>

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<menu id="menu_add"> <menu id="menu_add">
<section> <section>

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<menu id="menu_app"> <menu id="menu_app">
<section> <section>

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<menu id="menu_conversation"> <menu id="menu_conversation">
<section> <section>

View file

@ -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>

View file

@ -17,12 +17,47 @@ window.dino-main .dino-conversation undershoot {
background: none; 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; background: @theme_base_color;
} }
window.dino-main .dino-chatinput frame box:backdrop { window.dino-main .dino-sidebar frame.auto-complete list > row {
background: @theme_unfocused_base_color; 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 { window.dino-main button.dino-chatinput-button {

View file

@ -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>

View file

@ -32,7 +32,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
window = new UnifiedWindow(this, stream_interactor); window = new UnifiedWindow(this, stream_interactor);
notifications = new Notifications(stream_interactor, window); notifications = new Notifications(stream_interactor, window);
notifications.start(); notifications.start();
notifications.conversation_selected.connect(window.on_conversation_selected); notifications.conversation_selected.connect((conversation) => window.on_conversation_selected(conversation));
} }
window.present(); window.present();
}); });

View file

@ -32,7 +32,7 @@ public class View : Box {
[GtkChild] private Separator file_separator; [GtkChild] private Separator file_separator;
private EncryptionButton encryption_widget = new EncryptionButton() { margin_top=3, valign=Align.START, visible=true }; 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; this.stream_interactor = stream_interactor;
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input); occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input);
@ -70,6 +70,7 @@ public class View : Box {
Util.force_css(frame, "* { border-radius: 3px; }"); Util.force_css(frame, "* { border-radius: 3px; }");
stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available); stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available);
return this;
} }
public void initialize_for_conversation(Conversation conversation) { public void initialize_for_conversation(Conversation conversation) {

View file

@ -10,7 +10,6 @@ public class ConversationListTitlebar : Gtk.HeaderBar {
public signal void conversation_opened(Conversation conversation); public signal void conversation_opened(Conversation conversation);
[GtkChild] private MenuButton add_button; [GtkChild] private MenuButton add_button;
[GtkChild] public ToggleButton search_button;
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;

View file

@ -10,43 +10,14 @@ namespace Dino.Ui.ConversationSelector {
public class View : Box { public class View : Box {
public List conversation_list; public List conversation_list;
[GtkChild] public SearchEntry search_entry;
[GtkChild] public Revealer search_revealer;
[GtkChild] private ScrolledWindow scrolled; [GtkChild] private ScrolledWindow scrolled;
public View(StreamInteractor stream_interactor) { public View init(StreamInteractor stream_interactor) {
conversation_list = new List(stream_interactor) { visible=true }; conversation_list = new List(stream_interactor) { visible=true };
scrolled.add(conversation_list); scrolled.add(conversation_list);
search_entry.key_release_event.connect(search_key_release_event); return this;
search_entry.search_changed.connect(search_changed);
} }
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;
}
} }
} }

View file

@ -6,7 +6,7 @@ using Xmpp;
namespace Dino.Ui.ConversationSummary { namespace Dino.Ui.ConversationSummary {
class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
public string id { get { return "chat_state"; } } 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_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) { private void update_chat_state(Account account, Jid jid) {
HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation); HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation);

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box {
return datetime.format(format); return datetime.format(format);
} }
public virtual string get_relative_time(DateTime datetime) { public static string get_relative_time(DateTime datetime) {
DateTime now = new DateTime.now_local(); DateTime now = new DateTime.now_local();
TimeSpan timespan = now.difference(datetime); TimeSpan timespan = now.difference(datetime);
if (timespan > 365 * TimeSpan.DAY) { if (timespan > 365 * TimeSpan.DAY) {

View file

@ -11,19 +11,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
public Conversation? conversation { get; private set; } public Conversation? conversation { get; private set; }
[GtkChild] private ScrolledWindow scrolled; [GtkChild] public ScrolledWindow scrolled;
[GtkChild] private Revealer notification_revealer; [GtkChild] private Revealer notification_revealer;
[GtkChild] private Box notifications; [GtkChild] private Box notifications;
[GtkChild] private Box main; [GtkChild] private Box main;
[GtkChild] private Stack stack; [GtkChild] private Stack stack;
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items); private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_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> 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, 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.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>(); private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
private MessagePopulator message_item_populator; private ContentProvider content_populator;
private SubscriptionNotitication subscription_notification; private SubscriptionNotitication subscription_notification;
private double? was_value; private double? was_value;
@ -33,22 +33,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
private Mutex reloading_mutex = Mutex(); private Mutex reloading_mutex = Mutex();
private bool animate = false; private bool animate = false;
private bool firstLoad = true; 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; this.stream_interactor = stream_interactor;
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
scrolled.vadjustment.notify["value"].connect(on_value_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); subscription_notification = new SubscriptionNotitication(stream_interactor);
insert_item.connect(on_insert_item); insert_item.connect(filter_insert_item);
remove_item.connect(on_remove_item); remove_item.connect(do_remove_item);
Application app = GLib.Application.get_default() as Application; Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor)); app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
app.plugin_registry.register_conversation_item_populator(new FilePopulator(stream_interactor)); app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor));
app.plugin_registry.register_conversation_item_populator(new DateSeparatorPopulator(stream_interactor));
Timeout.add_seconds(60, () => { Timeout.add_seconds(60, () => {
foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
@ -57,7 +58,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
return true; return true;
}); });
Util.force_base_background(this); return this;
} }
// Workaround GTK TextView issues: Delay first load of contents // Workaround GTK TextView issues: Delay first load of contents
@ -65,54 +66,127 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
if (firstLoad) { if (firstLoad) {
int timeout = firstLoad ? 1000 : 0; int timeout = firstLoad ? 1000 : 0;
Timeout.add(timeout, () => { Timeout.add(timeout, () => {
stack.set_visible_child_name("void");
initialize_for_conversation_(conversation); initialize_for_conversation_(conversation);
display_latest();
stack.set_visible_child_name("main");
return false; return false;
}); });
firstLoad = false; firstLoad = false;
} else { } else {
stack.set_visible_child_name("void");
initialize_for_conversation_(conversation); 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) { private void initialize_for_conversation_(Conversation? conversation) {
Dino.Application app = Dino.Application.get_default(); Dino.Application app = Dino.Application.get_default();
if (this.conversation != null) { 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); populator.close(conversation);
} }
} }
this.conversation = conversation; this.conversation = conversation;
stack.set_visible_child_name("void");
clear();
was_upper = null;
was_page_size = null;
animate = false;
Timeout.add(20, () => { animate = true; return false; });
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK); populator.init(conversation, this, Plugins.WidgetType.GTK);
} }
message_item_populator.init(conversation, this); content_populator.init(this, conversation, Plugins.WidgetType.GTK);
message_item_populator.populate_latest(conversation, 40);
Idle.add(() => { on_value_notify(); return false; });
subscription_notification.init(conversation, this); subscription_notification.init(conversation, this);
stack.set_visible_child_name("main"); animate = false;
Timeout.add(20, () => { animate = true; return false; });
} }
public void on_insert_item(Plugins.MetaConversationItem item) { private void display_latest() {
clear();
Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40);
foreach (ContentMetaItem item in items) {
do_insert_item(item);
}
Idle.add(() => { on_value_notify(); return false; });
}
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 do_insert_item(Plugins.MetaConversationItem item) {
lock (meta_items) { lock (meta_items) {
if (!item.can_merge || !merge_back(item)) { if (!item.can_merge || !merge_back(item)) {
insert_new(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) { private void do_remove_item(Plugins.MetaConversationItem item) {
lock (meta_items) {
ConversationItemSkeleton? skeleton = item_item_skeletons[item]; ConversationItemSkeleton? skeleton = item_item_skeletons[item];
if (skeleton != null) {
if (skeleton.items.size > 1) { if (skeleton.items.size > 1) {
skeleton.remove_meta_item(item); skeleton.remove_meta_item(item);
} else { } else {
@ -122,6 +196,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
item_skeletons.remove(skeleton); item_skeletons.remove(skeleton);
item_item_skeletons.unset(item); item_item_skeletons.unset(item);
} }
content_items.remove(item);
meta_items.remove(item); meta_items.remove(item);
} }
} }
@ -151,10 +226,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
lower_start_item.encryption == item.encryption && lower_start_item.encryption == item.encryption &&
(item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) { (item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) {
lower_skeleton.add_meta_item(item); 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; item_item_skeletons[item] = lower_skeleton;
meta_items.add(item);
return true; return true;
} }
@ -162,7 +237,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
return false; return false;
} }
private void insert_new(Plugins.MetaConversationItem item) { private Widget insert_new(Plugins.MetaConversationItem item) {
Plugins.MetaConversationItem? lower_item = meta_items.lower(item); Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
// Does another skeleton need to be split? // Does another skeleton need to be split?
@ -181,7 +256,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
item_item_skeletons[item] = item_skeleton; item_item_skeletons[item] = item_skeleton;
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
item_skeletons.insert(index, item_skeleton); item_skeletons.insert(index, item_skeleton);
meta_items.add(item);
// Insert widget // Insert widget
Widget insert = item_skeleton; Widget insert = item_skeleton;
@ -195,22 +269,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
main.add(insert); main.add(insert);
} }
widgets[item] = 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); 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 an item from the past was added, add everything between that item and the (post-)first present item
if (index == 0) { if (index == 0) {
Dino.Application app = Dino.Application.get_default(); Dino.Application app = Dino.Application.get_default();
if (item_skeletons.size == 1) { 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()); populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc());
} }
} else { } 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); 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) { 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) { while(i < split_skeleton.items.size) {
Plugins.MetaConversationItem meta_item = split_skeleton.items[i]; Plugins.MetaConversationItem meta_item = split_skeleton.items[i];
if (time.compare(meta_item.display_time) < 0) { if (time.compare(meta_item.display_time) < 0) {
remove_item(meta_item); do_remove_item(meta_item);
if (!already_divided) { if (!already_divided) {
insert_new(meta_item); insert_new(meta_item);
already_divided = true; already_divided = true;
} else { } else {
insert_item(meta_item); do_insert_item(meta_item);
} }
} }
i++; i++;
@ -232,51 +307,73 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
} }
private void on_upper_notify() { private void on_upper_notify() {
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 || if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
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 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) { } 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 scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
} }
was_upper = scrolled.vadjustment.upper; was_upper = scrolled.vadjustment.upper;
was_page_size = scrolled.vadjustment.page_size; was_page_size = scrolled.vadjustment.page_size;
was_value = scrolled.vadjustment.value;
reloading_mutex.trylock(); reloading_mutex.trylock();
reloading_mutex.unlock(); reloading_mutex.unlock();
} }
private void on_value_notify() { private void on_value_notify() {
if (scrolled.vadjustment.value < 200) { if (scrolled.vadjustment.value < 400) {
load_earlier_messages(); load_earlier_messages();
} else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
load_later_messages();
} }
} }
private void load_earlier_messages() { private void load_earlier_messages() {
was_value = scrolled.vadjustment.value; was_value = scrolled.vadjustment.value;
if (!reloading_mutex.trylock()) return; 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); int res = a.sort_time.compare(b.sort_time);
if (res == 0) { if (res == 0) {
if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1; if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) {
else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) res = 1; res = -1;
} else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) {
res = 1;
}
} }
return res; 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() { private void clear() {
was_upper = null;
was_page_size = null;
content_items.clear();
meta_items.clear(); meta_items.clear();
meta_after_items.clear();
item_skeletons.clear(); item_skeletons.clear();
item_item_skeletons.clear(); item_item_skeletons.clear();
widgets.clear(); widgets.clear();

View file

@ -6,7 +6,7 @@ using Xmpp;
namespace Dino.Ui.ConversationSummary { namespace Dino.Ui.ConversationSummary {
class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object { class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
public string id { get { return "date_separator"; } } 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_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) { private void on_insert_item(Plugins.MetaConversationItem item) {
if (item.display_time == null) return; if (item.display_time == null) return;

View file

@ -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();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}
}

View file

@ -24,7 +24,6 @@ public class MessageTextView : TextView {
motion_notify_event.connect(change_cursor_over_url); motion_notify_event.connect(change_cursor_over_url);
update_display_style(); update_display_style();
Util.force_base_background(this, "textview, text:not(:selected)");
style_updated.connect(update_display_style); style_updated.connect(update_display_style);
populate_popup.connect(populate_context_menu); populate_popup.connect(populate_context_menu);
} }
@ -60,7 +59,7 @@ public class MessageTextView : TextView {
TextIter end_iter; TextIter end_iter;
buffer.get_iter_at_offset(out start_iter, start); buffer.get_iter_at_offset(out start_iter, start);
buffer.get_iter_at_offset(out end_iter, end); 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; TextIter end_iter;
buffer.get_iter_at_offset(out start_iter, absolute_start + start); buffer.get_iter_at_offset(out start_iter, absolute_start + start);
buffer.get_iter_at_offset(out end_iter, absolute_start + end); 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);
} }
} }

View file

@ -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;
}
}
}

View file

@ -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) { }
}
}

View file

@ -11,6 +11,7 @@ public class ConversationTitlebar : Gtk.HeaderBar {
private Window window; private Window window;
private Conversation? conversation; private Conversation? conversation;
private Gee.List<Plugins.ConversationTitlebarWidget> widgets = new ArrayList<Plugins.ConversationTitlebarWidget>(); 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) { public ConversationTitlebar(StreamInteractor stream_interactor, Window window) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
@ -19,9 +20,11 @@ public class ConversationTitlebar : Gtk.HeaderBar {
this.get_style_context().add_class("dino-right"); this.get_style_context().add_class("dino-right");
show_close_button = true; show_close_button = true;
hexpand = 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; 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 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)); app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window));
foreach(var e in app.plugin_registry.conversation_titlebar_entries) { foreach(var e in app.plugin_registry.conversation_titlebar_entries) {

View file

@ -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;
}
}
}

View file

@ -1,11 +1,12 @@
using Gee; using Gee;
using Gdk;
using Gtk; using Gtk;
using Dino.Entities; using Dino.Entities;
namespace Dino.Ui { namespace Dino.Ui {
public class UnifiedWindow : Window { public class UnifiedWindow : Gtk.Window {
private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true }; private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true };
private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true }; private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true };
@ -16,7 +17,12 @@ public class UnifiedWindow : Window {
private ConversationTitlebar conversation_titlebar; private ConversationTitlebar conversation_titlebar;
private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true }; 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 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 Stack stack = new Stack() { visible=true };
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
@ -36,8 +42,47 @@ public class UnifiedWindow : Window {
setup_unified(); setup_unified();
setup_stack(); setup_stack();
conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_revealer, "reveal-child", var vadjustment = conversation_frame.scrolled.vadjustment;
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 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); paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
focus_in_event.connect(on_focus_in_event); 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); }); 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.primary_button.clicked.connect(() => { get_application().activate_action("add_chat", null); });
conversations_placeholder.secondary_button.clicked.connect(() => { get_application().activate_action("add_conference", 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); filterable_conversation_list.conversation_list.conversation_selected.connect((conversation) => on_conversation_selected(conversation));
conversation_list_titlebar.conversation_opened.connect(on_conversation_selected); conversation_list_titlebar.conversation_opened.connect((conversation) => on_conversation_selected(conversation));
check_stack(); 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)) { if (this.conversation == null || !this.conversation.equals(conversation)) {
this.conversation = conversation; this.conversation = conversation;
stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation); stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation);
conversation.active = true; // only for conversation_selected conversation.active = true; // only for conversation_selected
filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened 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); chat_input.initialize_for_conversation(conversation);
if (default_initialize_conversation) {
conversation_frame.initialize_for_conversation(conversation); conversation_frame.initialize_for_conversation(conversation);
}
conversation_titlebar.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() { private void setup_unified() {
chat_input = new ChatInput.View(stream_interactor) { visible=true }; Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui");
conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true }; paned = (Paned) builder.get_object("paned");
filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true }; chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor);
conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).init(stream_interactor);
Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true }; filterable_conversation_list = ((ConversationSelector.View) builder.get_object("conversation_list")).init(stream_interactor);
grid.get_style_context().add_class("dino-conversation"); goto_end_revealer = (Revealer) builder.get_object("goto_end_revealer");
grid.add(conversation_frame); goto_end_button = (Button) builder.get_object("goto_end_button");
grid.add(chat_input); search_box = ((GlobalSearch) builder.get_object("search_box")).init(stream_interactor);
search_revealer = (Revealer) builder.get_object("search_revealer");
paned.set_position(300); search_entry = (SearchEntry) builder.get_object("search_entry");
paned.pack1(filterable_conversation_list, false, false);
paned.pack2(grid, true, false);
} }
private void setup_headerbar() { private void setup_headerbar() {

View file

@ -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)); 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 = "*") { public static void force_color(Gtk.Widget widget, string color, string selector = "*") {
force_css(widget, force_color_css.printf(selector, color)); 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 == " "; 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);
}
} }

View file

@ -90,7 +90,7 @@ public class FileProvider : Dino.FileProvider, Object {
file_transfer.state = FileTransfer.State.NOT_STARTED; file_transfer.state = FileTransfer.State.NOT_STARTED;
file_transfer.provider = 0; file_transfer.provider = 0;
file_transfer.info = message.id.to_string(); file_transfer.info = message.id.to_string();
file_incoming(file_transfer); file_incoming(file_transfer, conversation);
success = true; success = true;
Idle.add((owned)callback); Idle.add((owned)callback);
}); });

View file

@ -81,28 +81,25 @@ public class Manager : StreamInteractionModule, FileSender, Object {
} }
} }
public class FileMessageFilterDisplay : Plugins.MessageDisplayProvider, Object { public class FileMessageFilter : ContentFilter, Object {
public string id { get; set; default="file_message_filter"; }
public double priority { get; set; default=10; }
public Database db; public Database db;
public FileMessageFilterDisplay(Dino.Database db) { public FileMessageFilter(Dino.Database db) {
this.db = db; this.db = db;
} }
public bool can_display(Entities.Message? message) { public bool discard(ContentItem content_item) {
return message_is_file(db, message); if (content_item.type_ == MessageItem.TYPE) {
MessageItem message_item = content_item as MessageItem;
return message_is_file(db, message_item.message);
} }
return false;
public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
return null;
} }
} }
private bool message_is_file(Database db, Entities.Message message) { 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 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().with(db.file_transfer.info, "=", message.body); 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; return builder.count() > 0 || builder2.count() > 0;
} }

View file

@ -19,7 +19,7 @@ public class Plugin : RootInterface, Object {
}); });
app.stream_interactor.get_module(FileManager.IDENTITY).add_provider(file_provider); app.stream_interactor.get_module(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() { public void shutdown() {

View file

@ -3,6 +3,8 @@ using Sqlite;
namespace Qlite { namespace Qlite {
public abstract class Column<T> { public abstract class Column<T> {
public const string DEFALT_TABLE_NAME = "";
public string name { get; private set; } public string name { get; private set; }
public string? default { get; set; } public string? default { get; set; }
public int sqlite_type { get; private 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 virtual bool not_null { get; set; }
public long min_version { get; set; default = -1; } public long min_version { get; set; default = -1; }
public long max_version { get; set; default = long.MAX; } 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; return false;
} }
internal abstract void bind(Statement stmt, int index, T value); internal abstract void bind(Statement stmt, int index, T value);
public string to_string() { public string to_string() {
return table == null ? name : (table.name + "." + name);
}
public string to_column_definition() {
string res = name; string res = name;
switch (sqlite_type) { switch (sqlite_type) {
case INTEGER: case INTEGER:
@ -58,12 +65,12 @@ public abstract class Column<T> {
base(name, INTEGER); base(name, INTEGER);
} }
public override int get(Row row) { public override int get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return (int) row.get_integer(name); return (int) row.get_integer(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 !row.has_integer(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) { internal override void bind(Statement stmt, int index, int value) {
@ -76,12 +83,12 @@ public abstract class Column<T> {
base(name, INTEGER); base(name, INTEGER);
} }
public override long get(Row row) { public override long get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return (long) row.get_integer(name); return (long) row.get_integer(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 !row.has_integer(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) { internal override void bind(Statement stmt, int index, long value) {
@ -94,12 +101,12 @@ public abstract class Column<T> {
base(name, FLOAT); base(name, FLOAT);
} }
public override double get(Row row) { public override double get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return row.get_real(name); return row.get_real(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 !row.has_real(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) { internal override void bind(Statement stmt, int index, double value) {
@ -112,12 +119,12 @@ public abstract class Column<T> {
base(name, TEXT); base(name, TEXT);
} }
public override string? get(Row row) { public override string? get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return row.get_text(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 get(row) == null; return get(row, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == null;
} }
internal override void bind(Statement stmt, int index, string? value) { 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 bool not_null { get { return true; } set {} }
public override string get(Row row) { public override string get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return (!)row.get_text(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; return false;
} }
@ -154,8 +161,8 @@ public abstract class Column<T> {
base(name, TEXT); base(name, TEXT);
} }
public override bool get(Row row) { public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return row.get_text(name) == "1"; 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) { internal override void bind(Statement stmt, int index, bool value) {
@ -168,8 +175,8 @@ public abstract class Column<T> {
base(name, INTEGER); base(name, INTEGER);
} }
public override bool get(Row row) { public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) {
return row.get_integer(name) == 1; 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) { internal override void bind(Statement stmt, int index, bool value) {

View file

@ -90,6 +90,11 @@ public class Database {
return new QueryBuilder(this).select(columns); return new QueryBuilder(this).select(columns);
} }
internal MatchQueryBuilder match_query(Table table) {
ensure_init();
return new MatchQueryBuilder(this, table);
}
public InsertBuilder insert() { public InsertBuilder insert() {
ensure_init(); ensure_init();
return new InsertBuilder(this); return new InsertBuilder(this);

View file

@ -10,16 +10,22 @@ public class QueryBuilder : StatementBuilder {
private Column[] columns = {}; private Column[] columns = {};
// FROM [...] // FROM [...]
private Table? table; protected Table? table;
private string? table_name; protected string? table_name;
// JOIN [...]
private string joins = "";
// WHERE [...] // WHERE [...]
private string selection = "1"; protected string selection = "1";
private StatementBuilder.AbstractField[] selection_args = {}; internal StatementBuilder.AbstractField[] selection_args = {};
// ORDER BY [...] // ORDER BY [...]
private OrderingTerm[]? order_by_terms = {}; private OrderingTerm[]? order_by_terms = {};
// GROUP BY [...]
private string? group_by_term;
// LIMIT [...] OFFSET [...] // LIMIT [...] OFFSET [...]
private int limit_val; private int limit_val;
private int offset_val; private int offset_val;
@ -30,12 +36,12 @@ public class QueryBuilder : StatementBuilder {
public QueryBuilder select(Column[] columns = {}) { public QueryBuilder select(Column[] columns = {}) {
this.columns = columns; this.columns = columns;
if (columns.length == 0) { if (columns.length != 0) {
for (int i = 0; i < columns.length; i++) { for (int i = 0; i < columns.length; i++) {
if (column_selector == "*") { if (column_selector == "*") {
column_selector = columns[0].name; column_selector = columns[i].to_string();
} else { } else {
column_selector += ", " + columns[i].name; column_selector += ", " + columns[i].to_string();
} }
} }
} else { } else {
@ -50,21 +56,45 @@ public class QueryBuilder : StatementBuilder {
return this; return this;
} }
public QueryBuilder from(Table table) { public virtual QueryBuilder from(Table table) {
if (this.table_name != null) error("cannot use from() multiple times."); if (this.table_name != null) error("cannot use from() multiple times.");
this.table = table; this.table = table;
this.table_name = table.name; this.table_name = table.name;
return this; return this;
} }
public QueryBuilder from_name(string table) { public virtual QueryBuilder from_name(string table) {
this.table_name = table; this.table_name = table;
return this; 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 = {}) { public QueryBuilder where(string selection, string[] selection_args = {}) {
if (this.selection != "1") error("selection was already done, but where() was called."); this.selection = @"($(this.selection)) AND ($selection)";
this.selection = selection;
foreach (string arg in selection_args) { foreach (string arg in selection_args) {
this.selection_args += new StatementBuilder.StringField(arg); 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) { public QueryBuilder with<T>(Column<T> column, string comp, T value) {
if ((column.unique || column.primary_key) && comp == "=") single_result = true; if ((column.unique || column.primary_key) && comp == "=") single_result = true;
selection_args += new Field<T>(column, value); selection_args += new Field<T>(column, value);
selection = @"($selection) AND $(column.name) $comp ?"; selection = @"($selection) AND $column $comp ?";
return this; return this;
} }
public QueryBuilder with_null<T>(Column<T> column) { public QueryBuilder with_null<T>(Column<T> column) {
selection = @"($selection) AND $(column.name) ISNULL"; selection = @"($selection) AND $column ISNULL";
return this; return this;
} }
public QueryBuilder without_null<T>(Column<T> column) { public QueryBuilder without_null<T>(Column<T> column) {
selection = @"($selection) AND $(column.name) NOT NULL"; selection = @"($selection) AND $column NOT NULL";
return this; return this;
} }
@ -98,6 +128,17 @@ public class QueryBuilder : StatementBuilder {
return this; 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) { public QueryBuilder limit(int limit) {
if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit"); if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit");
this.limit_val = limit; this.limit_val = limit;
@ -135,7 +176,7 @@ public class QueryBuilder : StatementBuilder {
} }
internal override Statement prepare() { 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++) { for (int i = 0; i < selection_args.length; i++) {
selection_args[i].bind(stmt, i+1); selection_args[i].bind(stmt, i+1);
} }
@ -147,13 +188,13 @@ public class QueryBuilder : StatementBuilder {
} }
class OrderingTerm { class OrderingTerm {
Column column; Column? column;
string column_name; string column_name;
string dir; string dir;
public OrderingTerm(Column column, string dir) { public OrderingTerm(Column column, string dir) {
this.column = column; this.column = column;
this.column_name = column.name; this.column_name = column.to_string();
this.dir = dir; 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;
}
}
} }

View file

@ -10,15 +10,21 @@ public class Row {
internal Row(Statement stmt) { internal Row(Statement stmt) {
for (int i = 0; i < stmt.column_count(); i++) { 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)) { switch(stmt.column_type(i)) {
case TEXT: case TEXT:
text_map[stmt.column_name(i)] = stmt.column_text(i); text_map[column_name] = stmt.column_text(i);
break; break;
case INTEGER: case INTEGER:
int_map[stmt.column_name(i)] = (long) stmt.column_int64(i); int_map[column_name] = (long) stmt.column_int64(i);
break; break;
case FLOAT: case FLOAT:
real_map[stmt.column_name(i)] = stmt.column_double(i); real_map[column_name] = stmt.column_double(i);
break; break;
} }
} }
@ -28,27 +34,54 @@ public class Row {
return field[this]; return field[this];
} }
public string? get_text(string field) { private string field_name(string field, string? table) {
if (text_map.has_key(field)) { if (table != null) {
return text_map[field]; 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; return null;
} }
public long get_integer(string field) { public long get_integer(string field, string? table = null) {
return int_map[field]; return int_map[field_name(field, table)];
} }
public bool has_integer(string field) { public bool has_integer(string field, string? table = null) {
return int_map.has_key(field); return int_map.has_key(field_name(field, table));
} }
public double get_real(string field, double def = 0) { public double get_real(string field, string? table = null, double def = 0) {
return real_map[field] ?? def; return real_map[field_name(field, table)] ?? def;
} }
public bool has_real(string field) { public bool has_real(string field, string? table = null) {
return real_map.has_key(field) && real_map[field] != null; return real_map.has_key(field_name(field, table)) && real_map[field_name(field, table)] != null;
}
public string to_string() {
string ret = "{";
foreach (string key in text_map.keys) {
if (ret.length > 1) ret += ", ";
ret = @"$ret$key: \"$(text_map[key])\"";
}
foreach (string key in int_map.keys) {
if (ret.length > 1) ret += ", ";
ret = @"$ret$key: $(int_map[key])";
}
foreach (string key in real_map.keys) {
if (ret.length > 1) ret += ", ";
ret = @"$ret$key: $(real_map[key])";
}
return ret + "}";
} }
} }

View file

@ -8,6 +8,8 @@ public class Table {
protected Column[]? columns; protected Column[]? columns;
private string constraints = ""; private string constraints = "";
private string[] post_statements = {}; private string[] post_statements = {};
private string[] create_statements = {};
internal Column[]? fts_columns;
public Table(Database db, string name) { public Table(Database db, string name) {
this.db = db; this.db = db;
@ -17,6 +19,37 @@ public class Table {
public void init(Column[] columns, string constraints = "") { public void init(Column[] columns, string constraints = "") {
this.columns = columns; this.columns = columns;
this.constraints = constraints; 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) { public void unique(Column[] columns, string? on_conflict = null) {
@ -37,6 +70,10 @@ public class Table {
post_statements += stmt; post_statements += stmt;
} }
public void add_create_statement(string stmt) {
create_statements += stmt;
}
public void index(string index_name, Column[] columns, bool unique = false) { public void index(string index_name, Column[] columns, bool unique = false) {
string stmt = @"CREATE $(unique ? "UNIQUE" : "") INDEX IF NOT EXISTS $index_name ON $name ("; string stmt = @"CREATE $(unique ? "UNIQUE" : "") INDEX IF NOT EXISTS $index_name ON $name (";
bool first = true; bool first = true;
@ -58,6 +95,15 @@ public class Table {
return db.select(columns).from(this); 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() { public InsertBuilder insert() {
ensure_init(); ensure_init();
return db.insert().into(this); return db.insert().into(this);
@ -98,7 +144,7 @@ public class Table {
for (int i = 0; i < columns.length; i++) { for (int i = 0; i < columns.length; i++) {
Column c = columns[i]; Column c = columns[i];
if (c.min_version <= version && c.max_version >= version) { if (c.min_version <= version && c.max_version >= version) {
sql += @"$(i > 0 ? "," : "") $c"; sql += @"$(i > 0 ? "," : "") $(c.to_column_definition())";
} }
} }
sql += @"$constraints)"; sql += @"$constraints)";
@ -107,6 +153,13 @@ public class Table {
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Create table at version"); 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) { public void add_columns_for_version(long old_version, long new_version) {
@ -114,7 +167,7 @@ public class Table {
foreach (Column c in columns) { foreach (Column c in columns) {
if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version) { if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version) {
try { try {
db.exec(@"ALTER TABLE $name ADD COLUMN $c"); db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())");
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Add columns for version"); error("Qlite Error: Add columns for version");
} }