commit
ecb18afdb5
|
@ -29,6 +29,7 @@ SOURCES
|
|||
src/service/blocking_manager.vala
|
||||
src/service/chat_interaction.vala
|
||||
src/service/connection_manager.vala
|
||||
src/service/content_item_store.vala
|
||||
src/service/conversation_manager.vala
|
||||
src/service/counterpart_interaction_manager.vala
|
||||
src/service/database.vala
|
||||
|
@ -42,6 +43,7 @@ SOURCES
|
|||
src/service/presence_manager.vala
|
||||
src/service/registration.vala
|
||||
src/service/roster_manager.vala
|
||||
src/service/search_processor.vala
|
||||
src/service/stream_interactor.vala
|
||||
src/service/util.vala
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@ public interface Dino.Application : GLib.Application {
|
|||
ChatInteraction.start(stream_interactor);
|
||||
FileManager.start(stream_interactor, db);
|
||||
NotificationEvents.start(stream_interactor);
|
||||
ContentItemStore.start(stream_interactor, db);
|
||||
SearchProcessor.start(stream_interactor, db);
|
||||
|
||||
create_actions();
|
||||
|
||||
|
|
|
@ -23,7 +23,21 @@ public class FileTransfer : Object {
|
|||
public DateTime? local_time { get; set; }
|
||||
public Encryption encryption { get; set; }
|
||||
|
||||
public InputStream input_stream { get; set; }
|
||||
private InputStream? input_stream_ = null;
|
||||
public InputStream input_stream {
|
||||
get {
|
||||
if (input_stream_ == null) {
|
||||
File file = File.new_for_path(Path.build_filename(storage_dir, path ?? file_name));
|
||||
try {
|
||||
input_stream_ = file.read();
|
||||
} catch (Error e) { }
|
||||
}
|
||||
return input_stream_;
|
||||
}
|
||||
set {
|
||||
input_stream_ = value;
|
||||
}
|
||||
}
|
||||
public OutputStream output_stream { get; set; }
|
||||
|
||||
public string file_name { get; set; }
|
||||
|
@ -41,9 +55,11 @@ public class FileTransfer : Object {
|
|||
public string info { get; set; }
|
||||
|
||||
private Database? db;
|
||||
private string storage_dir;
|
||||
|
||||
public FileTransfer.from_row(Database db, Qlite.Row row) {
|
||||
public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) {
|
||||
this.db = db;
|
||||
this.storage_dir = storage_dir;
|
||||
|
||||
id = row[db.file_transfer.id];
|
||||
account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO don’t have to generate acc new
|
||||
|
@ -61,7 +77,7 @@ public class FileTransfer : Object {
|
|||
}
|
||||
direction = row[db.file_transfer.direction];
|
||||
time = new DateTime.from_unix_utc(row[db.file_transfer.time]);
|
||||
local_time = new DateTime.from_unix_utc(row[db.file_transfer.time]);
|
||||
local_time = new DateTime.from_unix_utc(row[db.file_transfer.local_time]);
|
||||
encryption = (Encryption) row[db.file_transfer.encryption];
|
||||
file_name = row[db.file_transfer.file_name];
|
||||
path = row[db.file_transfer.path];
|
||||
|
|
|
@ -82,7 +82,7 @@ public class Message : Object {
|
|||
}
|
||||
direction = row[db.message.direction];
|
||||
time = new DateTime.from_unix_utc(row[db.message.time]);
|
||||
local_time = new DateTime.from_unix_utc(row[db.message.time]);
|
||||
local_time = new DateTime.from_unix_utc(row[db.message.local_time]);
|
||||
body = row[db.message.body];
|
||||
marked = (Message.Marked) row[db.message.marked];
|
||||
encryption = (Encryption) row[db.message.encryption];
|
||||
|
|
|
@ -75,15 +75,16 @@ public interface ConversationTitlebarWidget : Object {
|
|||
public abstract interface ConversationItemPopulator : Object {
|
||||
public abstract string id { get; }
|
||||
public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type);
|
||||
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
|
||||
public virtual void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
|
||||
public abstract void close(Conversation conversation);
|
||||
}
|
||||
|
||||
public abstract interface ConversationAdditionPopulator : ConversationItemPopulator {
|
||||
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
|
||||
}
|
||||
|
||||
public abstract class MetaConversationItem : Object {
|
||||
public virtual string populator_id { get; set; }
|
||||
public virtual Jid? jid { get; set; default=null; }
|
||||
public virtual string color { get; set; default=null; }
|
||||
public virtual string display_name { get; set; default=null; }
|
||||
public virtual bool dim { get; set; default=false; }
|
||||
public virtual DateTime? sort_time { get; set; default=null; }
|
||||
public virtual double seccondary_sort_indicator { get; set; }
|
||||
|
@ -103,21 +104,4 @@ public interface ConversationItemCollection : Object {
|
|||
public signal void remove_item(MetaConversationItem item);
|
||||
}
|
||||
|
||||
public interface MessageDisplayProvider : Object {
|
||||
public abstract string id { get; set; }
|
||||
public abstract double priority { get; set; }
|
||||
public abstract bool can_display(Entities.Message? message);
|
||||
public abstract MetaConversationItem? get_item(Entities.Message message, Entities.Conversation conversation);
|
||||
}
|
||||
|
||||
public interface FileWidget : Object {
|
||||
public abstract Object? get_widget(WidgetType type);
|
||||
}
|
||||
|
||||
public interface FileDisplayProvider : Object {
|
||||
public abstract double priority { get; }
|
||||
public abstract bool can_display(Entities.Message? message);
|
||||
public abstract FileWidget? get_item(Entities.Message? message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,8 +7,7 @@ public class Registry {
|
|||
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
|
||||
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
|
||||
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
|
||||
internal Gee.List<MessageDisplayProvider> message_displays = new ArrayList<MessageDisplayProvider>();
|
||||
internal Gee.List<ConversationItemPopulator> conversation_item_populators = new ArrayList<ConversationItemPopulator>();
|
||||
internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
|
||||
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
|
||||
if (a.order < b.order) {
|
||||
return -1;
|
||||
|
@ -70,22 +69,12 @@ public class Registry {
|
|||
}
|
||||
}
|
||||
|
||||
public bool register_message_display(MessageDisplayProvider provider) {
|
||||
lock (message_displays) {
|
||||
foreach(MessageDisplayProvider p in message_displays) {
|
||||
if (p.id == provider.id) return false;
|
||||
}
|
||||
message_displays.add(provider);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool register_conversation_item_populator(ConversationItemPopulator populator) {
|
||||
lock (conversation_item_populators) {
|
||||
foreach(ConversationItemPopulator p in conversation_item_populators) {
|
||||
public bool register_conversation_addition_populator(ConversationAdditionPopulator populator) {
|
||||
lock (conversation_addition_populators) {
|
||||
foreach(ConversationItemPopulator p in conversation_addition_populators) {
|
||||
if (p.id == populator.id) return false;
|
||||
}
|
||||
conversation_item_populators.add(populator);
|
||||
conversation_addition_populators.add(populator);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
245
libdino/src/service/content_item_store.vala
Normal file
245
libdino/src/service/content_item_store.vala
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,7 +9,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
|
|||
public string id { get { return IDENTITY.id; } }
|
||||
|
||||
public signal void received_state(Account account, Jid jid, string state);
|
||||
public signal void received_marker(Account account, Jid jid, Entities.Message message, string marker);
|
||||
public signal void received_marker(Account account, Jid jid, Entities.Message message, Entities.Message.Marked marker);
|
||||
public signal void received_message_received(Account account, Jid jid, Entities.Message message);
|
||||
public signal void received_message_displayed(Account account, Jid jid, Entities.Message message);
|
||||
|
||||
|
@ -69,12 +69,12 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
|
|||
if (marker != Xep.ChatMarkers.MARKER_DISPLAYED && marker != Xep.ChatMarkers.MARKER_ACKNOWLEDGED) return;
|
||||
Conversation? conversation = stream_interactor.get_module(MessageStorage.IDENTITY).get_conversation_for_stanza_id(account, stanza_id);
|
||||
if (conversation == null) return;
|
||||
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation);
|
||||
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
|
||||
if (message == null) return;
|
||||
conversation.read_up_to = message;
|
||||
} else {
|
||||
foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_conversations(jid, account)) {
|
||||
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(stanza_id, conversation);
|
||||
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
|
||||
if (message != null) {
|
||||
switch (marker) {
|
||||
case Xep.ChatMarkers.MARKER_RECEIVED:
|
||||
|
|
|
@ -6,7 +6,7 @@ using Dino.Entities;
|
|||
namespace Dino {
|
||||
|
||||
public class Database : Qlite.Database {
|
||||
private const int VERSION = 6;
|
||||
private const int VERSION = 8;
|
||||
|
||||
public class AccountTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
|
@ -34,6 +34,21 @@ public class Database : Qlite.Database {
|
|||
}
|
||||
}
|
||||
|
||||
public class ContentTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<int> conversation_id = new Column.Integer("conversation_id") { not_null = true };
|
||||
public Column<long> time = new Column.Long("time") { not_null = true };
|
||||
public Column<long> local_time = new Column.Long("local_time") { not_null = true };
|
||||
public Column<int> content_type = new Column.Integer("content_type") { not_null = true };
|
||||
public Column<int> foreign_id = new Column.Integer("foreign_id") { not_null = true };
|
||||
|
||||
internal ContentTable(Database db) {
|
||||
base(db, "content_item");
|
||||
init({id, conversation_id, time, local_time, content_type, foreign_id});
|
||||
unique({content_type, foreign_id}, "IGNORE");
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageTable : Table {
|
||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||
public Column<string> stanza_id = new Column.Text("stanza_id");
|
||||
|
@ -54,6 +69,7 @@ public class Database : Qlite.Database {
|
|||
init({id, stanza_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
|
||||
type_, time, local_time, body, encryption, marked});
|
||||
index("message_localtime_counterpart_idx", {local_time, counterpart_id});
|
||||
fts({body});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,6 +189,7 @@ public class Database : Qlite.Database {
|
|||
|
||||
public AccountTable account { get; private set; }
|
||||
public JidTable jid { get; private set; }
|
||||
public ContentTable content { get; private set; }
|
||||
public MessageTable message { get; private set; }
|
||||
public RealJidTable real_jid { get; private set; }
|
||||
public FileTransferTable file_transfer { get; private set; }
|
||||
|
@ -190,6 +207,7 @@ public class Database : Qlite.Database {
|
|||
base(fileName, VERSION);
|
||||
account = new AccountTable(this);
|
||||
jid = new JidTable(this);
|
||||
content = new ContentTable(this);
|
||||
message = new MessageTable(this);
|
||||
real_jid = new RealJidTable(this);
|
||||
file_transfer = new FileTransferTable(this);
|
||||
|
@ -198,7 +216,7 @@ public class Database : Qlite.Database {
|
|||
entity_feature = new EntityFeatureTable(this);
|
||||
roster = new RosterTable(this);
|
||||
settings = new SettingsTable(this);
|
||||
init({ account, jid, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings });
|
||||
init({ account, jid, content, message, real_jid, file_transfer, conversation, avatar, entity_feature, roster, settings });
|
||||
try {
|
||||
exec("PRAGMA synchronous=0");
|
||||
} catch (Error e) { }
|
||||
|
@ -206,6 +224,31 @@ public class Database : Qlite.Database {
|
|||
|
||||
public override void migrate(long oldVersion) {
|
||||
// new table columns are added, outdated columns are still present
|
||||
if (oldVersion < 7) {
|
||||
message.fts_rebuild();
|
||||
} else if (oldVersion < 8) {
|
||||
exec("""
|
||||
insert into content_item (conversation_id, time, local_time, content_type, foreign_id)
|
||||
select conversation.id, message.time, message.local_time, 1, message.id
|
||||
from message join conversation on
|
||||
message.account_id=conversation.account_id and
|
||||
message.counterpart_id=conversation.jid_id and
|
||||
message.type=conversation.type+1 and
|
||||
(message.counterpart_resource=conversation.resource or message.type != 3)
|
||||
where
|
||||
message.body not in (select info from file_transfer where info not null) and
|
||||
message.id not in (select info from file_transfer where info not null)
|
||||
union
|
||||
select conversation.id, message.time, message.local_time, 2, file_transfer.id
|
||||
from file_transfer
|
||||
join message on
|
||||
file_transfer.info=message.id
|
||||
join conversation on
|
||||
file_transfer.account_id=conversation.account_id and
|
||||
file_transfer.counterpart_id=conversation.jid_id and
|
||||
message.type=conversation.type+1 and
|
||||
(message.counterpart_resource=conversation.resource or message.type != 3)""");
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<Account> get_accounts() {
|
||||
|
@ -232,11 +275,41 @@ public class Database : Qlite.Database {
|
|||
}
|
||||
}
|
||||
|
||||
public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before) {
|
||||
QueryBuilder select = message.select()
|
||||
.with(message.counterpart_id, "=", get_jid_id(jid))
|
||||
public int add_content_item(Conversation conversation, DateTime time, DateTime local_time, int content_type, int foreign_id) {
|
||||
return (int) content.insert()
|
||||
.value(content.conversation_id, conversation.id)
|
||||
.value(content.local_time, (long) local_time.to_unix())
|
||||
.value(content.time, (long) time.to_unix())
|
||||
.value(content.content_type, content_type)
|
||||
.value(content.foreign_id, foreign_id)
|
||||
.perform();
|
||||
}
|
||||
|
||||
public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) {
|
||||
QueryBuilder select = message.select();
|
||||
|
||||
if (before != null) {
|
||||
if (id > 0) {
|
||||
select.where(@"local_time < ? OR (local_time = ? AND id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() });
|
||||
} else {
|
||||
select.with(message.id, "<", id);
|
||||
}
|
||||
}
|
||||
if (after != null) {
|
||||
if (id > 0) {
|
||||
select.where(@"local_time > ? OR (local_time = ? AND id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() });
|
||||
} else {
|
||||
select.with(message.local_time, ">", (long) after.to_unix());
|
||||
}
|
||||
if (id > 0) {
|
||||
select.with(message.id, ">", id);
|
||||
}
|
||||
} else {
|
||||
select.order_by(message.id, "DESC");
|
||||
}
|
||||
|
||||
select.with(message.counterpart_id, "=", get_jid_id(jid))
|
||||
.with(message.account_id, "=", account.id)
|
||||
.order_by(message.id, "DESC")
|
||||
.limit(count);
|
||||
if (jid.resourcepart != null) {
|
||||
select.with(message.counterpart_resource, "=", jid.resourcepart);
|
||||
|
@ -244,9 +317,6 @@ public class Database : Qlite.Database {
|
|||
if (type != null) {
|
||||
select.with(message.type_, "=", (int) type);
|
||||
}
|
||||
if (before != null) {
|
||||
select.with(message.local_time, "<", (long) before.to_unix());
|
||||
}
|
||||
|
||||
LinkedList<Message> ret = new LinkedList<Message>();
|
||||
foreach (Row row in select) {
|
||||
|
|
|
@ -11,7 +11,7 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
public string id { get { return IDENTITY.id; } }
|
||||
|
||||
public signal void upload_available(Account account);
|
||||
public signal void received_file(FileTransfer file_transfer);
|
||||
public signal void received_file(FileTransfer file_transfer, Conversation conversation);
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Database db;
|
||||
|
@ -68,7 +68,7 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
file_sender.send_file(conversation, file_transfer);
|
||||
}
|
||||
}
|
||||
received_file(file_transfer);
|
||||
received_file(file_transfer, conversation);
|
||||
}
|
||||
|
||||
public bool is_upload_available(Conversation conversation) {
|
||||
|
@ -78,21 +78,38 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
return false;
|
||||
}
|
||||
|
||||
public Gee.List<FileTransfer> get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) {
|
||||
public Gee.List<FileTransfer> get_latest_transfers(Account account, Jid counterpart, int n) {
|
||||
Qlite.QueryBuilder select = db.file_transfer.select()
|
||||
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
|
||||
.with(db.file_transfer.account_id, "=", account.id)
|
||||
.order_by(db.file_transfer.local_time, "DESC")
|
||||
.limit(n);
|
||||
return get_transfers_from_qry(select);
|
||||
}
|
||||
|
||||
public Gee.List<FileTransfer> get_transfers_before(Account account, Jid counterpart, DateTime before, int n) {
|
||||
Qlite.QueryBuilder select = db.file_transfer.select()
|
||||
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
|
||||
.with(db.file_transfer.account_id, "=", account.id)
|
||||
.with(db.file_transfer.local_time, "<", (long)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()
|
||||
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
|
||||
.with(db.file_transfer.account_id, "=", account.id)
|
||||
.with(db.file_transfer.local_time, ">", (long)after.to_unix())
|
||||
.with(db.file_transfer.local_time, "<", (long)before.to_unix())
|
||||
.order_by(db.file_transfer.id, "DESC");
|
||||
.limit(n);
|
||||
return get_transfers_from_qry(select);
|
||||
}
|
||||
|
||||
private Gee.List<FileTransfer> get_transfers_from_qry(Qlite.QueryBuilder select) {
|
||||
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
|
||||
foreach (Qlite.Row row in select) {
|
||||
FileTransfer file_transfer = new FileTransfer.from_row(db, row);
|
||||
File file = File.new_for_path(Path.build_filename(get_storage_dir(), file_transfer.path ?? file_transfer.file_name));
|
||||
try {
|
||||
file_transfer.input_stream = file.read();
|
||||
} catch (Error e) { }
|
||||
FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());
|
||||
ret.insert(0, file_transfer);
|
||||
}
|
||||
return ret;
|
||||
|
@ -117,7 +134,7 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
outgoing_processors.add(processor);
|
||||
}
|
||||
|
||||
private void handle_incomming_file(FileTransfer file_transfer) {
|
||||
private void handle_incomming_file(FileTransfer file_transfer, Conversation conversation) {
|
||||
foreach (IncommingFileProcessor processor in incomming_processors) {
|
||||
if (processor.can_process(file_transfer)) {
|
||||
processor.process(file_transfer);
|
||||
|
@ -131,7 +148,7 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
} catch (Error e) { }
|
||||
|
||||
file_transfer.persist(db);
|
||||
received_file(file_transfer);
|
||||
received_file(file_transfer, conversation);
|
||||
}
|
||||
|
||||
private void save_file(FileTransfer file_transfer) {
|
||||
|
@ -152,7 +169,7 @@ public class FileManager : StreamInteractionModule, Object {
|
|||
}
|
||||
|
||||
public interface FileProvider : Object {
|
||||
public signal void file_incoming(FileTransfer file_transfer);
|
||||
public signal void file_incoming(FileTransfer file_transfer, Conversation conversation);
|
||||
}
|
||||
|
||||
public interface FileSender : Object {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Gee;
|
||||
using Qlite;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
|
@ -51,26 +52,47 @@ public class MessageStorage : StreamInteractionModule, Object {
|
|||
return null;
|
||||
}
|
||||
|
||||
public Gee.List<Message>? get_messages_before_message(Conversation? conversation, Message message, int count = 20) {
|
||||
SortedSet<Message>? before = messages[conversation].head_set(message);
|
||||
if (before != null && before.size >= count) {
|
||||
Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
|
||||
Iterator<Message> iter = before.iterator();
|
||||
iter.next();
|
||||
for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next();
|
||||
while(iter.has_next()) {
|
||||
Message m = iter.get();
|
||||
ret.add(m);
|
||||
iter.next();
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, message.local_time);
|
||||
return db_messages;
|
||||
public Gee.List<MessageItem> get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) {
|
||||
// SortedSet<Message>? before = messages[conversation].head_set(message);
|
||||
// if (before != null && before.size >= count) {
|
||||
// Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
|
||||
// Iterator<Message> iter = before.iterator();
|
||||
// iter.next();
|
||||
// for (int from_index = before.size - count; iter.has_next() && from_index > 0; from_index--) iter.next();
|
||||
// while(iter.has_next()) {
|
||||
// Message m = iter.get();
|
||||
// ret.add(m);
|
||||
// iter.next();
|
||||
// }
|
||||
// return ret;
|
||||
// } else {
|
||||
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id);
|
||||
Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
|
||||
foreach (Message message in db_messages) {
|
||||
ret.add(new MessageItem(message, conversation, -1));
|
||||
}
|
||||
return ret;
|
||||
// }
|
||||
}
|
||||
|
||||
public 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);
|
||||
foreach (Message message in messages[conversation]) {
|
||||
if (message.stanza_id == stanza_id) return message;
|
||||
|
@ -100,7 +122,7 @@ public class MessageStorage : StreamInteractionModule, Object {
|
|||
}
|
||||
return res;
|
||||
});
|
||||
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null);
|
||||
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null, null, -1);
|
||||
messages[conversation].add_all(db_messages);
|
||||
}
|
||||
}
|
||||
|
|
263
libdino/src/service/search_processor.vala
Normal file
263
libdino/src/service/search_processor.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,7 @@ set(RESOURCE_LIST
|
|||
chat_input.ui
|
||||
contact_details_dialog.ui
|
||||
conversation_list_titlebar.ui
|
||||
global_search.ui
|
||||
conversation_selector/view.ui
|
||||
conversation_selector/chat_row_tooltip.ui
|
||||
conversation_selector/conversation_row.ui
|
||||
|
@ -43,7 +44,9 @@ set(RESOURCE_LIST
|
|||
menu_encryption.ui
|
||||
occupant_list.ui
|
||||
occupant_list_item.ui
|
||||
search_autocomplete.ui
|
||||
settings_dialog.ui
|
||||
unified_main_content.ui
|
||||
unified_window_placeholder.ui
|
||||
|
||||
theme.css
|
||||
|
@ -93,6 +96,7 @@ SOURCES
|
|||
src/ui/contact_details/dialog.vala
|
||||
src/ui/contact_details/muc_config_form_provider.vala
|
||||
src/ui/conversation_list_titlebar.vala
|
||||
src/ui/global_search.vala
|
||||
src/ui/conversation_selector/chat_row.vala
|
||||
src/ui/conversation_selector/conversation_row.vala
|
||||
src/ui/conversation_selector/groupchat_pm_row.vala
|
||||
|
@ -100,19 +104,16 @@ SOURCES
|
|||
src/ui/conversation_selector/list.vala
|
||||
src/ui/conversation_selector/view.vala
|
||||
src/ui/conversation_summary/chat_state_populator.vala
|
||||
src/ui/conversation_summary/content_item_widget_factory.vala
|
||||
src/ui/conversation_summary/content_populator.vala
|
||||
src/ui/conversation_summary/conversation_item_skeleton.vala
|
||||
src/ui/conversation_summary/conversation_view.vala
|
||||
src/ui/conversation_summary/date_separator_populator.vala
|
||||
src/ui/conversation_summary/default_file_display.vala
|
||||
src/ui/conversation_summary/default_message_display.vala
|
||||
src/ui/conversation_summary/file_populator.vala
|
||||
src/ui/conversation_summary/image_display.vala
|
||||
src/ui/conversation_summary/message_populator.vala
|
||||
src/ui/conversation_summary/message_textview.vala
|
||||
src/ui/conversation_summary/slashme_message_display.vala
|
||||
src/ui/conversation_summary/subscription_notification.vala
|
||||
src/ui/conversation_titlebar/menu_entry.vala
|
||||
src/ui/conversation_titlebar/occupants_entry.vala
|
||||
src/ui/conversation_titlebar/search_entry.vala
|
||||
src/ui/conversation_titlebar/view.vala
|
||||
src/ui/manage_accounts/account_row.vala
|
||||
src/ui/manage_accounts/add_account_dialog.vala
|
||||
|
|
|
@ -22,20 +22,5 @@
|
|||
<property name="pack_type">start</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="search_button">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
<property name="icon-size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
|
@ -4,21 +4,6 @@
|
|||
<property name="can_focus">True</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="search_revealer">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="placeholder_text" translatable="yes">Search</property>
|
||||
<property name="margin">10px</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled">
|
||||
<property name="expand">True</property>
|
||||
|
|
169
main/data/global_search.ui
Normal file
169
main/data/global_search.ui
Normal 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>
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_add">
|
||||
<section>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_app">
|
||||
<section>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_conversation">
|
||||
<section>
|
||||
|
|
24
main/data/search_autocomplete.ui
Normal file
24
main/data/search_autocomplete.ui
Normal 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>
|
|
@ -17,12 +17,47 @@ window.dino-main .dino-conversation undershoot {
|
|||
background: none;
|
||||
}
|
||||
|
||||
window.dino-main .dino-chatinput frame box {
|
||||
@keyframes highlight {
|
||||
from { background: alpha(@warning_color, 0.5) }
|
||||
to { background: transparent }
|
||||
}
|
||||
|
||||
window.dino-main .dino-conversation .highlight-once {
|
||||
animation-duration: 3s;
|
||||
animation-timing-function: ease-out;
|
||||
animation-iteration-count: 1;
|
||||
animation-name: highlight;
|
||||
}
|
||||
|
||||
window.dino-main .dino-conversation textview, window.dino-main .dino-conversation textview text {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
window.dino-main .dino-sidebar > frame {
|
||||
background: @insensitive_bg_color;
|
||||
border-left: 1px solid @borders;
|
||||
border-bottom: 1px solid @borders;
|
||||
}
|
||||
|
||||
window.dino-main .dino-sidebar > frame.collapsed {
|
||||
border-bottom: 1px solid @borders;
|
||||
}
|
||||
|
||||
window.dino-main .dino-sidebar frame.auto-complete {
|
||||
background: @theme_base_color;
|
||||
}
|
||||
|
||||
window.dino-main .dino-chatinput frame box:backdrop {
|
||||
background: @theme_unfocused_base_color;
|
||||
window.dino-main .dino-sidebar frame.auto-complete list > row {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
window.dino-main .dino-sidebar textview,
|
||||
window.dino-main .dino-sidebar textview text {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
window.dino-main .dino-chatinput frame box {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
window.dino-main button.dino-chatinput-button {
|
||||
|
|
100
main/data/unified_main_content.ui
Normal file
100
main/data/unified_main_content.ui
Normal 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>
|
|
@ -32,7 +32,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
|
|||
window = new UnifiedWindow(this, stream_interactor);
|
||||
notifications = new Notifications(stream_interactor, window);
|
||||
notifications.start();
|
||||
notifications.conversation_selected.connect(window.on_conversation_selected);
|
||||
notifications.conversation_selected.connect((conversation) => window.on_conversation_selected(conversation));
|
||||
}
|
||||
window.present();
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@ public class View : Box {
|
|||
[GtkChild] private Separator file_separator;
|
||||
private EncryptionButton encryption_widget = new EncryptionButton() { margin_top=3, valign=Align.START, visible=true };
|
||||
|
||||
public View(StreamInteractor stream_interactor) {
|
||||
public View init(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
|
||||
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input);
|
||||
|
@ -70,6 +70,7 @@ public class View : Box {
|
|||
Util.force_css(frame, "* { border-radius: 3px; }");
|
||||
|
||||
stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void initialize_for_conversation(Conversation conversation) {
|
||||
|
|
|
@ -10,7 +10,6 @@ public class ConversationListTitlebar : Gtk.HeaderBar {
|
|||
public signal void conversation_opened(Conversation conversation);
|
||||
|
||||
[GtkChild] private MenuButton add_button;
|
||||
[GtkChild] public ToggleButton search_button;
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
|
||||
|
|
|
@ -10,43 +10,14 @@ namespace Dino.Ui.ConversationSelector {
|
|||
public class View : Box {
|
||||
public List conversation_list;
|
||||
|
||||
[GtkChild] public SearchEntry search_entry;
|
||||
[GtkChild] public Revealer search_revealer;
|
||||
[GtkChild] private ScrolledWindow scrolled;
|
||||
|
||||
public View(StreamInteractor stream_interactor) {
|
||||
public View init(StreamInteractor stream_interactor) {
|
||||
conversation_list = new List(stream_interactor) { visible=true };
|
||||
scrolled.add(conversation_list);
|
||||
search_entry.key_release_event.connect(search_key_release_event);
|
||||
search_entry.search_changed.connect(search_changed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void conversation_selected(Conversation? conversation) {
|
||||
search_entry.set_text("");
|
||||
}
|
||||
|
||||
private void refilter() {
|
||||
string[]? values = null;
|
||||
string str = search_entry.get_text ();
|
||||
if (str != "") values = str.split(" ");
|
||||
conversation_list.set_filter_values(values);
|
||||
}
|
||||
|
||||
private void search_changed(Editable editable) {
|
||||
refilter();
|
||||
}
|
||||
|
||||
private bool search_key_release_event(EventKey event) {
|
||||
conversation_list.select_row(conversation_list.get_row_at_y(0));
|
||||
if (event.keyval == Key.Down) {
|
||||
ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0);
|
||||
if (row != null) {
|
||||
conversation_list.select_row(row);
|
||||
row.grab_focus();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ using Xmpp;
|
|||
|
||||
namespace Dino.Ui.ConversationSummary {
|
||||
|
||||
class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
|
||||
class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
|
||||
|
||||
public string id { get { return "chat_state"; } }
|
||||
|
||||
|
@ -43,8 +43,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
|
|||
|
||||
public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
|
||||
|
||||
public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
|
||||
|
||||
private void update_chat_state(Account account, Jid jid) {
|
||||
HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
110
main/src/ui/conversation_summary/content_populator.vala
Normal file
110
main/src/ui/conversation_summary/content_populator.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box {
|
|||
return datetime.format(format);
|
||||
}
|
||||
|
||||
public virtual string get_relative_time(DateTime datetime) {
|
||||
public static string get_relative_time(DateTime datetime) {
|
||||
DateTime now = new DateTime.now_local();
|
||||
TimeSpan timespan = now.difference(datetime);
|
||||
if (timespan > 365 * TimeSpan.DAY) {
|
||||
|
|
|
@ -11,19 +11,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
|
||||
public Conversation? conversation { get; private set; }
|
||||
|
||||
[GtkChild] private ScrolledWindow scrolled;
|
||||
[GtkChild] public ScrolledWindow scrolled;
|
||||
[GtkChild] private Revealer notification_revealer;
|
||||
[GtkChild] private Box notifications;
|
||||
[GtkChild] private Box main;
|
||||
[GtkChild] private Stack stack;
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items);
|
||||
private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>();
|
||||
private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
|
||||
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
|
||||
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
|
||||
private MessagePopulator message_item_populator;
|
||||
private ContentProvider content_populator;
|
||||
private SubscriptionNotitication subscription_notification;
|
||||
|
||||
private double? was_value;
|
||||
|
@ -33,22 +33,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
private Mutex reloading_mutex = Mutex();
|
||||
private bool animate = false;
|
||||
private bool firstLoad = true;
|
||||
private bool at_current_content = true;
|
||||
private bool reload_messages = true;
|
||||
|
||||
public ConversationView(StreamInteractor stream_interactor) {
|
||||
public ConversationView init(StreamInteractor stream_interactor) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
|
||||
scrolled.vadjustment.notify["value"].connect(on_value_notify);
|
||||
|
||||
message_item_populator = new MessagePopulator(stream_interactor);
|
||||
content_populator = new ContentProvider(stream_interactor);
|
||||
subscription_notification = new SubscriptionNotitication(stream_interactor);
|
||||
|
||||
insert_item.connect(on_insert_item);
|
||||
remove_item.connect(on_remove_item);
|
||||
insert_item.connect(filter_insert_item);
|
||||
remove_item.connect(do_remove_item);
|
||||
|
||||
Application app = GLib.Application.get_default() as Application;
|
||||
app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor));
|
||||
app.plugin_registry.register_conversation_item_populator(new FilePopulator(stream_interactor));
|
||||
app.plugin_registry.register_conversation_item_populator(new DateSeparatorPopulator(stream_interactor));
|
||||
app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
|
||||
app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor));
|
||||
|
||||
Timeout.add_seconds(60, () => {
|
||||
foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
|
||||
|
@ -57,7 +58,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
return true;
|
||||
});
|
||||
|
||||
Util.force_base_background(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Workaround GTK TextView issues: Delay first load of contents
|
||||
|
@ -65,54 +66,127 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
if (firstLoad) {
|
||||
int timeout = firstLoad ? 1000 : 0;
|
||||
Timeout.add(timeout, () => {
|
||||
stack.set_visible_child_name("void");
|
||||
initialize_for_conversation_(conversation);
|
||||
display_latest();
|
||||
stack.set_visible_child_name("main");
|
||||
return false;
|
||||
});
|
||||
firstLoad = false;
|
||||
} else {
|
||||
stack.set_visible_child_name("void");
|
||||
initialize_for_conversation_(conversation);
|
||||
display_latest();
|
||||
stack.set_visible_child_name("main");
|
||||
}
|
||||
}
|
||||
|
||||
public void initialize_around_message(Conversation conversation, ContentItem content_item) {
|
||||
stack.set_visible_child_name("void");
|
||||
clear();
|
||||
initialize_for_conversation_(conversation);
|
||||
Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
|
||||
foreach (ContentMetaItem item in before_items) {
|
||||
do_insert_item(item);
|
||||
}
|
||||
ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item);
|
||||
meta_item.can_merge = false;
|
||||
Widget w = insert_new(meta_item);
|
||||
content_items.add(meta_item);
|
||||
meta_items.add(meta_item);
|
||||
|
||||
Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40);
|
||||
foreach (ContentMetaItem item in after_items) {
|
||||
do_insert_item(item);
|
||||
}
|
||||
if (after_items.size == 40) {
|
||||
at_current_content = false;
|
||||
}
|
||||
{
|
||||
int h = 0, i = 0;
|
||||
main.@foreach((widget) => {
|
||||
if (i >= before_items.size) return;
|
||||
ConversationItemSkeleton? sk = widget as ConversationItemSkeleton;
|
||||
i += sk != null ? sk.items.size : 1;
|
||||
int minimum_height, natural_height;
|
||||
widget.get_preferred_height_for_width(main.get_allocated_width() - 2 * main.margin, out minimum_height, out natural_height);
|
||||
h += minimum_height + 15;
|
||||
});
|
||||
}
|
||||
|
||||
reload_messages = false;
|
||||
Timeout.add(700, () => {
|
||||
int h = 0, i = 0;
|
||||
main.@foreach((widget) => {
|
||||
if (i >= before_items.size) return;
|
||||
ConversationItemSkeleton? sk = widget as ConversationItemSkeleton;
|
||||
i += sk != null ? sk.items.size : 1;
|
||||
h += widget.get_allocated_height() + 15;
|
||||
});
|
||||
scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
|
||||
w.get_style_context().add_class("highlight-once");
|
||||
reload_messages = true;
|
||||
stack.set_visible_child_name("main");
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void initialize_for_conversation_(Conversation? conversation) {
|
||||
Dino.Application app = Dino.Application.get_default();
|
||||
if (this.conversation != null) {
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
||||
populator.close(conversation);
|
||||
}
|
||||
}
|
||||
this.conversation = conversation;
|
||||
stack.set_visible_child_name("void");
|
||||
clear();
|
||||
was_upper = null;
|
||||
was_page_size = null;
|
||||
animate = false;
|
||||
Timeout.add(20, () => { animate = true; return false; });
|
||||
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
||||
populator.init(conversation, this, Plugins.WidgetType.GTK);
|
||||
}
|
||||
message_item_populator.init(conversation, this);
|
||||
message_item_populator.populate_latest(conversation, 40);
|
||||
Idle.add(() => { on_value_notify(); return false; });
|
||||
|
||||
content_populator.init(this, conversation, Plugins.WidgetType.GTK);
|
||||
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) {
|
||||
if (!item.can_merge || !merge_back(item)) {
|
||||
insert_new(item);
|
||||
}
|
||||
}
|
||||
if (item as ContentMetaItem != null) {
|
||||
content_items.add(item);
|
||||
}
|
||||
meta_items.add(item);
|
||||
}
|
||||
|
||||
public void on_remove_item(Plugins.MetaConversationItem item) {
|
||||
lock (meta_items) {
|
||||
ConversationItemSkeleton? skeleton = item_item_skeletons[item];
|
||||
private void do_remove_item(Plugins.MetaConversationItem item) {
|
||||
ConversationItemSkeleton? skeleton = item_item_skeletons[item];
|
||||
if (skeleton != null) {
|
||||
if (skeleton.items.size > 1) {
|
||||
skeleton.remove_meta_item(item);
|
||||
} else {
|
||||
|
@ -122,6 +196,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
item_skeletons.remove(skeleton);
|
||||
item_item_skeletons.unset(item);
|
||||
}
|
||||
content_items.remove(item);
|
||||
meta_items.remove(item);
|
||||
}
|
||||
}
|
||||
|
@ -151,10 +226,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
lower_start_item.encryption == item.encryption &&
|
||||
(item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) {
|
||||
lower_skeleton.add_meta_item(item);
|
||||
force_alloc_width(lower_skeleton, main.get_allocated_width());
|
||||
Util.force_alloc_width(lower_skeleton, main.get_allocated_width());
|
||||
|
||||
widgets[item] = widgets[lower_start_item];
|
||||
item_item_skeletons[item] = lower_skeleton;
|
||||
meta_items.add(item);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -162,7 +237,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void insert_new(Plugins.MetaConversationItem item) {
|
||||
private Widget insert_new(Plugins.MetaConversationItem item) {
|
||||
Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
|
||||
|
||||
// Does another skeleton need to be split?
|
||||
|
@ -181,7 +256,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
item_item_skeletons[item] = item_skeleton;
|
||||
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
|
||||
item_skeletons.insert(index, item_skeleton);
|
||||
meta_items.add(item);
|
||||
|
||||
// Insert widget
|
||||
Widget insert = item_skeleton;
|
||||
|
@ -195,22 +269,23 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
main.add(insert);
|
||||
}
|
||||
widgets[item] = insert;
|
||||
force_alloc_width(insert, main.get_allocated_width());
|
||||
Util.force_alloc_width(insert, main.get_allocated_width());
|
||||
main.reorder_child(insert, index);
|
||||
|
||||
// If an item from the past was added, add everything between that item and the (post-)first present item
|
||||
if (index == 0) {
|
||||
Dino.Application app = Dino.Application.get_default();
|
||||
if (item_skeletons.size == 1) {
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
|
||||
foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
||||
populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc());
|
||||
}
|
||||
} else {
|
||||
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
|
||||
foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
||||
populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
return insert;
|
||||
}
|
||||
|
||||
private void split_at_time(ConversationItemSkeleton split_skeleton, DateTime time) {
|
||||
|
@ -219,12 +294,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
while(i < split_skeleton.items.size) {
|
||||
Plugins.MetaConversationItem meta_item = split_skeleton.items[i];
|
||||
if (time.compare(meta_item.display_time) < 0) {
|
||||
remove_item(meta_item);
|
||||
do_remove_item(meta_item);
|
||||
if (!already_divided) {
|
||||
insert_new(meta_item);
|
||||
already_divided = true;
|
||||
} else {
|
||||
insert_item(meta_item);
|
||||
do_insert_item(meta_item);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
|
@ -232,51 +307,73 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
}
|
||||
|
||||
private void on_upper_notify() {
|
||||
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 ||
|
||||
scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
|
||||
scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
|
||||
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
|
||||
if (at_current_content) {
|
||||
scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
|
||||
}
|
||||
} else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
|
||||
scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
|
||||
}
|
||||
was_upper = scrolled.vadjustment.upper;
|
||||
was_page_size = scrolled.vadjustment.page_size;
|
||||
was_value = scrolled.vadjustment.value;
|
||||
reloading_mutex.trylock();
|
||||
reloading_mutex.unlock();
|
||||
}
|
||||
|
||||
private void on_value_notify() {
|
||||
if (scrolled.vadjustment.value < 200) {
|
||||
if (scrolled.vadjustment.value < 400) {
|
||||
load_earlier_messages();
|
||||
} else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
|
||||
load_later_messages();
|
||||
}
|
||||
}
|
||||
|
||||
private void load_earlier_messages() {
|
||||
was_value = scrolled.vadjustment.value;
|
||||
if (!reloading_mutex.trylock()) return;
|
||||
if (meta_items.size > 0) message_item_populator.populate_before(conversation, meta_items.first(), 20);
|
||||
if (meta_items.size > 0) {
|
||||
Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20);
|
||||
foreach (ContentMetaItem item in items) {
|
||||
do_insert_item(item);
|
||||
}
|
||||
} else {
|
||||
reloading_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
|
||||
private void load_later_messages() {
|
||||
if (!reloading_mutex.trylock()) return;
|
||||
if (meta_items.size > 0 && !at_current_content) {
|
||||
Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20);
|
||||
if (items.size == 0) {
|
||||
at_current_content = true;
|
||||
}
|
||||
foreach (ContentMetaItem item in items) {
|
||||
do_insert_item(item);
|
||||
}
|
||||
} else {
|
||||
reloading_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
|
||||
int res = a.sort_time.compare(b.sort_time);
|
||||
if (res == 0) {
|
||||
if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1;
|
||||
else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) res = 1;
|
||||
if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) {
|
||||
res = -1;
|
||||
} else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) {
|
||||
res = 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Workaround GTK TextView issues
|
||||
private void force_alloc_width(Widget widget, int width) {
|
||||
Allocation alloc = Allocation();
|
||||
widget.get_preferred_width(out alloc.width, null);
|
||||
widget.get_preferred_height(out alloc.height, null);
|
||||
alloc.width = width;
|
||||
widget.size_allocate(alloc);
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
was_upper = null;
|
||||
was_page_size = null;
|
||||
content_items.clear();
|
||||
meta_items.clear();
|
||||
meta_after_items.clear();
|
||||
item_skeletons.clear();
|
||||
item_item_skeletons.clear();
|
||||
widgets.clear();
|
||||
|
|
|
@ -6,7 +6,7 @@ using Xmpp;
|
|||
|
||||
namespace Dino.Ui.ConversationSummary {
|
||||
|
||||
class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object {
|
||||
class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
|
||||
|
||||
public string id { get { return "date_separator"; } }
|
||||
|
||||
|
@ -35,8 +35,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object {
|
|||
|
||||
public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { }
|
||||
|
||||
public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
|
||||
|
||||
private void on_insert_item(Plugins.MetaConversationItem item) {
|
||||
if (item.display_time == null) return;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,7 +24,6 @@ public class MessageTextView : TextView {
|
|||
motion_notify_event.connect(change_cursor_over_url);
|
||||
|
||||
update_display_style();
|
||||
Util.force_base_background(this, "textview, text:not(:selected)");
|
||||
style_updated.connect(update_display_style);
|
||||
populate_popup.connect(populate_context_menu);
|
||||
}
|
||||
|
@ -60,7 +59,7 @@ public class MessageTextView : TextView {
|
|||
TextIter end_iter;
|
||||
buffer.get_iter_at_offset(out start_iter, start);
|
||||
buffer.get_iter_at_offset(out end_iter, end);
|
||||
buffer.apply_tag_by_name("semibold", start_iter, end_iter);
|
||||
buffer.apply_tag(bold_tag, start_iter, end_iter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +124,7 @@ public class MessageTextView : TextView {
|
|||
TextIter end_iter;
|
||||
buffer.get_iter_at_offset(out start_iter, absolute_start + start);
|
||||
buffer.get_iter_at_offset(out end_iter, absolute_start + end);
|
||||
buffer.apply_tag_by_name("url", start_iter, end_iter);
|
||||
buffer.apply_tag(link_tag, start_iter, end_iter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
30
main/src/ui/conversation_titlebar/search_entry.vala
Normal file
30
main/src/ui/conversation_titlebar/search_entry.vala
Normal 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) { }
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ public class ConversationTitlebar : Gtk.HeaderBar {
|
|||
private Window window;
|
||||
private Conversation? conversation;
|
||||
private Gee.List<Plugins.ConversationTitlebarWidget> widgets = new ArrayList<Plugins.ConversationTitlebarWidget>();
|
||||
public GlobalSearchButton search_button = new GlobalSearchButton() { visible = true };
|
||||
|
||||
public ConversationTitlebar(StreamInteractor stream_interactor, Window window) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
|
@ -19,9 +20,11 @@ public class ConversationTitlebar : Gtk.HeaderBar {
|
|||
this.get_style_context().add_class("dino-right");
|
||||
show_close_button = true;
|
||||
hexpand = true;
|
||||
search_button.set_image(new Gtk.Image.from_icon_name("system-search-symbolic", Gtk.IconSize.MENU) { visible = true });
|
||||
|
||||
Application app = GLib.Application.get_default() as Application;
|
||||
app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor));
|
||||
app.plugin_registry.register_contact_titlebar_entry(new SearchMenuEntry(search_button));
|
||||
app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window));
|
||||
|
||||
foreach(var e in app.plugin_registry.conversation_titlebar_entries) {
|
||||
|
|
259
main/src/ui/global_search.vala
Normal file
259
main/src/ui/global_search.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
using Gee;
|
||||
using Gdk;
|
||||
using Gtk;
|
||||
|
||||
using Dino.Entities;
|
||||
|
||||
namespace Dino.Ui {
|
||||
|
||||
public class UnifiedWindow : Window {
|
||||
public class UnifiedWindow : Gtk.Window {
|
||||
|
||||
private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true };
|
||||
private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true };
|
||||
|
@ -16,7 +17,12 @@ public class UnifiedWindow : Window {
|
|||
private ConversationTitlebar conversation_titlebar;
|
||||
private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true };
|
||||
private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true };
|
||||
private Paned paned = new Paned(Orientation.HORIZONTAL) { visible=true };
|
||||
private Paned paned;
|
||||
private Revealer goto_end_revealer;
|
||||
private Button goto_end_button;
|
||||
private Revealer search_revealer;
|
||||
private SearchEntry search_entry;
|
||||
private GlobalSearch search_box;
|
||||
private Stack stack = new Stack() { visible=true };
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
|
@ -36,8 +42,47 @@ public class UnifiedWindow : Window {
|
|||
setup_unified();
|
||||
setup_stack();
|
||||
|
||||
conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_revealer, "reveal-child",
|
||||
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
|
||||
var vadjustment = conversation_frame.scrolled.vadjustment;
|
||||
vadjustment.notify["value"].connect(() => {
|
||||
goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size;
|
||||
});
|
||||
goto_end_button.clicked.connect(() => {
|
||||
conversation_frame.initialize_for_conversation(conversation);
|
||||
});
|
||||
|
||||
conversation_titlebar.search_button.clicked.connect(() => {
|
||||
search_revealer.reveal_child = conversation_titlebar.search_button.active;
|
||||
});
|
||||
search_revealer.notify["child-revealed"].connect(() => {
|
||||
if (search_revealer.child_revealed) {
|
||||
if (conversation_frame.conversation != null && search_box.search_entry.text == "") {
|
||||
reset_search_entry();
|
||||
}
|
||||
search_box.search_entry.grab_focus();
|
||||
}
|
||||
});
|
||||
search_box.selected_item.connect((item) => {
|
||||
on_conversation_selected(item.conversation, false, false);
|
||||
conversation_frame.initialize_around_message(item.conversation, item);
|
||||
close_search();
|
||||
});
|
||||
event.connect((event) => {
|
||||
if (event.type == EventType.BUTTON_PRESS) {
|
||||
int dest_x, dest_y;
|
||||
bool ret = search_box.translate_coordinates(this, 0, 0, out dest_x, out dest_y);
|
||||
int geometry_x, geometry_y, geometry_width, geometry_height;
|
||||
this.get_window().get_geometry(out geometry_x, out geometry_y, out geometry_width, out geometry_height);
|
||||
if (ret && event.button.x_root - geometry_x < dest_x || event.button.y_root - geometry_y < dest_y) {
|
||||
close_search();
|
||||
}
|
||||
} else if (event.type == EventType.KEY_RELEASE) {
|
||||
if (event.key.keyval == Gdk.Key.Escape) {
|
||||
close_search();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
|
||||
|
||||
focus_in_event.connect(on_focus_in_event);
|
||||
|
@ -50,38 +95,60 @@ public class UnifiedWindow : Window {
|
|||
accounts_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("accounts", null); });
|
||||
conversations_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("add_chat", null); });
|
||||
conversations_placeholder.secondary_button.clicked.connect(() => { get_application().activate_action("add_conference", null); });
|
||||
filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected);
|
||||
conversation_list_titlebar.conversation_opened.connect(on_conversation_selected);
|
||||
filterable_conversation_list.conversation_list.conversation_selected.connect((conversation) => on_conversation_selected(conversation));
|
||||
conversation_list_titlebar.conversation_opened.connect((conversation) => on_conversation_selected(conversation));
|
||||
|
||||
check_stack();
|
||||
}
|
||||
|
||||
public void on_conversation_selected(Conversation conversation) {
|
||||
private void reset_search_entry() {
|
||||
if (conversation_frame.conversation != null) {
|
||||
switch (conversation.type_) {
|
||||
case Conversation.Type.CHAT:
|
||||
case Conversation.Type.GROUPCHAT_PM:
|
||||
search_box.search_entry.text = @"with:$(conversation.counterpart) ";
|
||||
break;
|
||||
case Conversation.Type.GROUPCHAT:
|
||||
search_box.search_entry.text = @"in:$(conversation.counterpart) ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void on_conversation_selected(Conversation conversation, bool do_reset_search = true, bool default_initialize_conversation = true) {
|
||||
if (this.conversation == null || !this.conversation.equals(conversation)) {
|
||||
this.conversation = conversation;
|
||||
stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation);
|
||||
conversation.active = true; // only for conversation_selected
|
||||
filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened
|
||||
|
||||
if (do_reset_search) {
|
||||
reset_search_entry();
|
||||
}
|
||||
chat_input.initialize_for_conversation(conversation);
|
||||
conversation_frame.initialize_for_conversation(conversation);
|
||||
if (default_initialize_conversation) {
|
||||
conversation_frame.initialize_for_conversation(conversation);
|
||||
}
|
||||
conversation_titlebar.initialize_for_conversation(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private void close_search() {
|
||||
conversation_titlebar.search_button.active = false;
|
||||
search_revealer.reveal_child = false;
|
||||
}
|
||||
|
||||
private void setup_unified() {
|
||||
chat_input = new ChatInput.View(stream_interactor) { visible=true };
|
||||
conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true };
|
||||
filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true };
|
||||
|
||||
Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true };
|
||||
grid.get_style_context().add_class("dino-conversation");
|
||||
grid.add(conversation_frame);
|
||||
grid.add(chat_input);
|
||||
|
||||
paned.set_position(300);
|
||||
paned.pack1(filterable_conversation_list, false, false);
|
||||
paned.pack2(grid, true, false);
|
||||
Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui");
|
||||
paned = (Paned) builder.get_object("paned");
|
||||
chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor);
|
||||
conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).init(stream_interactor);
|
||||
filterable_conversation_list = ((ConversationSelector.View) builder.get_object("conversation_list")).init(stream_interactor);
|
||||
goto_end_revealer = (Revealer) builder.get_object("goto_end_revealer");
|
||||
goto_end_button = (Button) builder.get_object("goto_end_button");
|
||||
search_box = ((GlobalSearch) builder.get_object("search_box")).init(stream_interactor);
|
||||
search_revealer = (Revealer) builder.get_object("search_revealer");
|
||||
search_entry = (SearchEntry) builder.get_object("search_entry");
|
||||
}
|
||||
|
||||
private void setup_headerbar() {
|
||||
|
|
|
@ -118,10 +118,6 @@ public static void force_background(Gtk.Widget widget, string color, string sele
|
|||
force_css(widget, force_background_css.printf(selector, color));
|
||||
}
|
||||
|
||||
public static void force_base_background(Gtk.Widget widget, string selector = "*") {
|
||||
force_background(widget, "@theme_base_color", selector);
|
||||
}
|
||||
|
||||
public static void force_color(Gtk.Widget widget, string color, string selector = "*") {
|
||||
force_css(widget, force_color_css.printf(selector, color));
|
||||
}
|
||||
|
@ -142,4 +138,13 @@ public static bool is_24h_format() {
|
|||
return settings_format == "24h" || p_format == " ";
|
||||
}
|
||||
|
||||
// Workaround GTK TextView issues
|
||||
public static void force_alloc_width(Widget widget, int width) {
|
||||
Allocation alloc = Allocation();
|
||||
widget.get_preferred_width(out alloc.width, null);
|
||||
widget.get_preferred_height(out alloc.height, null);
|
||||
alloc.width = width;
|
||||
widget.size_allocate(alloc);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ public class FileProvider : Dino.FileProvider, Object {
|
|||
file_transfer.state = FileTransfer.State.NOT_STARTED;
|
||||
file_transfer.provider = 0;
|
||||
file_transfer.info = message.id.to_string();
|
||||
file_incoming(file_transfer);
|
||||
file_incoming(file_transfer, conversation);
|
||||
success = true;
|
||||
Idle.add((owned)callback);
|
||||
});
|
||||
|
|
|
@ -81,28 +81,25 @@ public class Manager : StreamInteractionModule, FileSender, Object {
|
|||
}
|
||||
}
|
||||
|
||||
public class FileMessageFilterDisplay : Plugins.MessageDisplayProvider, Object {
|
||||
public string id { get; set; default="file_message_filter"; }
|
||||
public double priority { get; set; default=10; }
|
||||
|
||||
public class FileMessageFilter : ContentFilter, Object {
|
||||
public Database db;
|
||||
|
||||
public FileMessageFilterDisplay(Dino.Database db) {
|
||||
public FileMessageFilter(Dino.Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public bool can_display(Entities.Message? message) {
|
||||
return message_is_file(db, message);
|
||||
}
|
||||
|
||||
public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
|
||||
return null;
|
||||
public bool discard(ContentItem content_item) {
|
||||
if (content_item.type_ == MessageItem.TYPE) {
|
||||
MessageItem message_item = content_item as MessageItem;
|
||||
return message_is_file(db, message_item.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool message_is_file(Database db, Entities.Message message) {
|
||||
Qlite.QueryBuilder builder = db.file_transfer.select().with(db.file_transfer.info, "=", message.id.to_string());
|
||||
Qlite.QueryBuilder builder2 = db.file_transfer.select().with(db.file_transfer.info, "=", message.body);
|
||||
Qlite.QueryBuilder builder = db.file_transfer.select({db.file_transfer.id}).with(db.file_transfer.info, "=", message.id.to_string());
|
||||
Qlite.QueryBuilder builder2 = db.file_transfer.select({db.file_transfer.id}).with(db.file_transfer.info, "=", message.body);
|
||||
return builder.count() > 0 || builder2.count() > 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ public class Plugin : RootInterface, Object {
|
|||
});
|
||||
|
||||
app.stream_interactor.get_module(FileManager.IDENTITY).add_provider(file_provider);
|
||||
app.plugin_registry.register_message_display(new FileMessageFilterDisplay(app.db));
|
||||
app.stream_interactor.get_module(ContentItemStore.IDENTITY).add_filter(new FileMessageFilter(app.db));
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
|
|
|
@ -3,6 +3,8 @@ using Sqlite;
|
|||
namespace Qlite {
|
||||
|
||||
public abstract class Column<T> {
|
||||
public const string DEFALT_TABLE_NAME = "";
|
||||
|
||||
public string name { get; private set; }
|
||||
public string? default { get; set; }
|
||||
public int sqlite_type { get; private set; }
|
||||
|
@ -12,16 +14,21 @@ public abstract class Column<T> {
|
|||
public virtual bool not_null { get; set; }
|
||||
public long min_version { get; set; default = -1; }
|
||||
public long max_version { get; set; default = long.MAX; }
|
||||
internal Table table { get; set; }
|
||||
|
||||
public abstract T get(Row row);
|
||||
public abstract T get(Row row, string? table_name = DEFALT_TABLE_NAME);
|
||||
|
||||
public virtual bool is_null(Row row) {
|
||||
public virtual bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
internal abstract void bind(Statement stmt, int index, T value);
|
||||
|
||||
public string to_string() {
|
||||
return table == null ? name : (table.name + "." + name);
|
||||
}
|
||||
|
||||
public string to_column_definition() {
|
||||
string res = name;
|
||||
switch (sqlite_type) {
|
||||
case INTEGER:
|
||||
|
@ -58,12 +65,12 @@ public abstract class Column<T> {
|
|||
base(name, INTEGER);
|
||||
}
|
||||
|
||||
public override int get(Row row) {
|
||||
return (int) row.get_integer(name);
|
||||
public override int get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return (int) row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
public override bool is_null(Row row) {
|
||||
return !row.has_integer(name);
|
||||
public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return !row.has_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, int value) {
|
||||
|
@ -76,12 +83,12 @@ public abstract class Column<T> {
|
|||
base(name, INTEGER);
|
||||
}
|
||||
|
||||
public override long get(Row row) {
|
||||
return (long) row.get_integer(name);
|
||||
public override long get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return (long) row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
public override bool is_null(Row row) {
|
||||
return !row.has_integer(name);
|
||||
public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return !row.has_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, long value) {
|
||||
|
@ -94,12 +101,12 @@ public abstract class Column<T> {
|
|||
base(name, FLOAT);
|
||||
}
|
||||
|
||||
public override double get(Row row) {
|
||||
return row.get_real(name);
|
||||
public override double get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return row.get_real(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
public override bool is_null(Row row) {
|
||||
return !row.has_real(name);
|
||||
public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return !row.has_real(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, double value) {
|
||||
|
@ -112,12 +119,12 @@ public abstract class Column<T> {
|
|||
base(name, TEXT);
|
||||
}
|
||||
|
||||
public override string? get(Row row) {
|
||||
return row.get_text(name);
|
||||
public override string? get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
public override bool is_null(Row row) {
|
||||
return get(row) == null;
|
||||
public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return get(row, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == null;
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, string? value) {
|
||||
|
@ -136,11 +143,11 @@ public abstract class Column<T> {
|
|||
|
||||
public override bool not_null { get { return true; } set {} }
|
||||
|
||||
public override string get(Row row) {
|
||||
return (!)row.get_text(name);
|
||||
public override string get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return (!)row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name);
|
||||
}
|
||||
|
||||
public override bool is_null(Row row) {
|
||||
public override bool is_null(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -154,8 +161,8 @@ public abstract class Column<T> {
|
|||
base(name, TEXT);
|
||||
}
|
||||
|
||||
public override bool get(Row row) {
|
||||
return row.get_text(name) == "1";
|
||||
public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return row.get_text(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == "1";
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, bool value) {
|
||||
|
@ -168,8 +175,8 @@ public abstract class Column<T> {
|
|||
base(name, INTEGER);
|
||||
}
|
||||
|
||||
public override bool get(Row row) {
|
||||
return row.get_integer(name) == 1;
|
||||
public override bool get(Row row, string? table_name = DEFALT_TABLE_NAME) {
|
||||
return row.get_integer(name, table_name == DEFALT_TABLE_NAME ? table.name : table_name) == 1;
|
||||
}
|
||||
|
||||
internal override void bind(Statement stmt, int index, bool value) {
|
||||
|
|
|
@ -90,6 +90,11 @@ public class Database {
|
|||
return new QueryBuilder(this).select(columns);
|
||||
}
|
||||
|
||||
internal MatchQueryBuilder match_query(Table table) {
|
||||
ensure_init();
|
||||
return new MatchQueryBuilder(this, table);
|
||||
}
|
||||
|
||||
public InsertBuilder insert() {
|
||||
ensure_init();
|
||||
return new InsertBuilder(this);
|
||||
|
|
|
@ -10,16 +10,22 @@ public class QueryBuilder : StatementBuilder {
|
|||
private Column[] columns = {};
|
||||
|
||||
// FROM [...]
|
||||
private Table? table;
|
||||
private string? table_name;
|
||||
protected Table? table;
|
||||
protected string? table_name;
|
||||
|
||||
// JOIN [...]
|
||||
private string joins = "";
|
||||
|
||||
// WHERE [...]
|
||||
private string selection = "1";
|
||||
private StatementBuilder.AbstractField[] selection_args = {};
|
||||
protected string selection = "1";
|
||||
internal StatementBuilder.AbstractField[] selection_args = {};
|
||||
|
||||
// ORDER BY [...]
|
||||
private OrderingTerm[]? order_by_terms = {};
|
||||
|
||||
// GROUP BY [...]
|
||||
private string? group_by_term;
|
||||
|
||||
// LIMIT [...] OFFSET [...]
|
||||
private int limit_val;
|
||||
private int offset_val;
|
||||
|
@ -30,12 +36,12 @@ public class QueryBuilder : StatementBuilder {
|
|||
|
||||
public QueryBuilder select(Column[] columns = {}) {
|
||||
this.columns = columns;
|
||||
if (columns.length == 0) {
|
||||
if (columns.length != 0) {
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
if (column_selector == "*") {
|
||||
column_selector = columns[0].name;
|
||||
column_selector = columns[i].to_string();
|
||||
} else {
|
||||
column_selector += ", " + columns[i].name;
|
||||
column_selector += ", " + columns[i].to_string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -50,21 +56,45 @@ public class QueryBuilder : StatementBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder from(Table table) {
|
||||
public virtual QueryBuilder from(Table table) {
|
||||
if (this.table_name != null) error("cannot use from() multiple times.");
|
||||
this.table = table;
|
||||
this.table_name = table.name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder from_name(string table) {
|
||||
public virtual QueryBuilder from_name(string table) {
|
||||
this.table_name = table;
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder outer_join_with<T>(Table table, Column<T> lhs, Column<T> rhs, string? as = null) {
|
||||
return outer_join_on(table, @"$lhs = $rhs", as);
|
||||
}
|
||||
|
||||
public QueryBuilder outer_join_on(Table table, string on, string? as = null) {
|
||||
if (as == null) as = table.name;
|
||||
joins += @" LEFT OUTER JOIN $(table.name) AS $as ON $on";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder join_with<T>(Table table, Column<T> lhs, Column<T> rhs, string? as = null) {
|
||||
return join_on(table, @"$lhs = $rhs", as);
|
||||
}
|
||||
|
||||
public QueryBuilder join_on(Table table, string on, string? as = null) {
|
||||
if (as == null) as = table.name;
|
||||
joins += @" JOIN $(table.name) AS $as ON $on";
|
||||
return this;
|
||||
}
|
||||
|
||||
internal QueryBuilder join_name(string table_name, string on) {
|
||||
joins += @" JOIN $table_name ON $on";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder where(string selection, string[] selection_args = {}) {
|
||||
if (this.selection != "1") error("selection was already done, but where() was called.");
|
||||
this.selection = selection;
|
||||
this.selection = @"($(this.selection)) AND ($selection)";
|
||||
foreach (string arg in selection_args) {
|
||||
this.selection_args += new StatementBuilder.StringField(arg);
|
||||
}
|
||||
|
@ -74,17 +104,17 @@ public class QueryBuilder : StatementBuilder {
|
|||
public QueryBuilder with<T>(Column<T> column, string comp, T value) {
|
||||
if ((column.unique || column.primary_key) && comp == "=") single_result = true;
|
||||
selection_args += new Field<T>(column, value);
|
||||
selection = @"($selection) AND $(column.name) $comp ?";
|
||||
selection = @"($selection) AND $column $comp ?";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder with_null<T>(Column<T> column) {
|
||||
selection = @"($selection) AND $(column.name) ISNULL";
|
||||
selection = @"($selection) AND $column ISNULL";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder without_null<T>(Column<T> column) {
|
||||
selection = @"($selection) AND $(column.name) NOT NULL";
|
||||
selection = @"($selection) AND $column NOT NULL";
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -98,6 +128,17 @@ public class QueryBuilder : StatementBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder group_by(Column[] columns) {
|
||||
foreach(Column col in columns) {
|
||||
if (group_by_term == null) {
|
||||
group_by_term = col.to_string();
|
||||
} else {
|
||||
group_by_term += @", $col";
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder limit(int limit) {
|
||||
if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit");
|
||||
this.limit_val = limit;
|
||||
|
@ -135,7 +176,7 @@ public class QueryBuilder : StatementBuilder {
|
|||
}
|
||||
|
||||
internal override Statement prepare() {
|
||||
Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") WHERE $selection $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")");
|
||||
Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") $joins WHERE $selection $(group_by_term == null ? "" : @"GROUP BY $group_by_term") $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")");
|
||||
for (int i = 0; i < selection_args.length; i++) {
|
||||
selection_args[i].bind(stmt, i+1);
|
||||
}
|
||||
|
@ -147,13 +188,13 @@ public class QueryBuilder : StatementBuilder {
|
|||
}
|
||||
|
||||
class OrderingTerm {
|
||||
Column column;
|
||||
Column? column;
|
||||
string column_name;
|
||||
string dir;
|
||||
|
||||
public OrderingTerm(Column column, string dir) {
|
||||
this.column = column;
|
||||
this.column_name = column.name;
|
||||
this.column_name = column.to_string();
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
|
@ -177,4 +218,21 @@ public class QueryBuilder : StatementBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
public class MatchQueryBuilder : QueryBuilder {
|
||||
internal MatchQueryBuilder(Database db, Table table) {
|
||||
base(db);
|
||||
if (table.fts_columns == null) error("MATCH query on non FTS table");
|
||||
from(table);
|
||||
join_name(@"_fts_$table_name", @"_fts_$table_name.docid = $table_name.rowid");
|
||||
}
|
||||
|
||||
public MatchQueryBuilder match(Column<string> column, string match) {
|
||||
if (table == null) error("MATCH must occur after FROM statement");
|
||||
if (!(column in table.fts_columns)) error("MATCH selection on non FTS column");
|
||||
selection_args += new StatementBuilder.StringField(match);
|
||||
selection = @"($selection) AND _fts_$table_name.$(column.name) MATCH ?";
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,15 +10,21 @@ public class Row {
|
|||
|
||||
internal Row(Statement stmt) {
|
||||
for (int i = 0; i < stmt.column_count(); i++) {
|
||||
string column_name;
|
||||
if (stmt.column_origin_name(i) != null) {
|
||||
column_name = @"$(stmt.column_table_name(i)).$(stmt.column_origin_name(i))";
|
||||
} else {
|
||||
column_name = stmt.column_name(i);
|
||||
}
|
||||
switch(stmt.column_type(i)) {
|
||||
case TEXT:
|
||||
text_map[stmt.column_name(i)] = stmt.column_text(i);
|
||||
text_map[column_name] = stmt.column_text(i);
|
||||
break;
|
||||
case INTEGER:
|
||||
int_map[stmt.column_name(i)] = (long) stmt.column_int64(i);
|
||||
int_map[column_name] = (long) stmt.column_int64(i);
|
||||
break;
|
||||
case FLOAT:
|
||||
real_map[stmt.column_name(i)] = stmt.column_double(i);
|
||||
real_map[column_name] = stmt.column_double(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -28,27 +34,54 @@ public class Row {
|
|||
return field[this];
|
||||
}
|
||||
|
||||
public string? get_text(string field) {
|
||||
if (text_map.has_key(field)) {
|
||||
return text_map[field];
|
||||
private string field_name(string field, string? table) {
|
||||
if (table != null) {
|
||||
return @"$table.$field";
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
public string? get_text(string field, string? table = null) {
|
||||
if (text_map.has_key(field_name(field, table))) {
|
||||
return text_map[field_name(field, table)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public long get_integer(string field) {
|
||||
return int_map[field];
|
||||
public long get_integer(string field, string? table = null) {
|
||||
return int_map[field_name(field, table)];
|
||||
}
|
||||
|
||||
public bool has_integer(string field) {
|
||||
return int_map.has_key(field);
|
||||
public bool has_integer(string field, string? table = null) {
|
||||
return int_map.has_key(field_name(field, table));
|
||||
}
|
||||
|
||||
public double get_real(string field, double def = 0) {
|
||||
return real_map[field] ?? def;
|
||||
public double get_real(string field, string? table = null, double def = 0) {
|
||||
return real_map[field_name(field, table)] ?? def;
|
||||
}
|
||||
|
||||
public bool has_real(string field) {
|
||||
return real_map.has_key(field) && real_map[field] != null;
|
||||
public bool has_real(string field, string? table = null) {
|
||||
return real_map.has_key(field_name(field, table)) && real_map[field_name(field, table)] != null;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
string ret = "{";
|
||||
|
||||
foreach (string key in text_map.keys) {
|
||||
if (ret.length > 1) ret += ", ";
|
||||
ret = @"$ret$key: \"$(text_map[key])\"";
|
||||
}
|
||||
foreach (string key in int_map.keys) {
|
||||
if (ret.length > 1) ret += ", ";
|
||||
ret = @"$ret$key: $(int_map[key])";
|
||||
}
|
||||
foreach (string key in real_map.keys) {
|
||||
if (ret.length > 1) ret += ", ";
|
||||
ret = @"$ret$key: $(real_map[key])";
|
||||
}
|
||||
|
||||
return ret + "}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ public class Table {
|
|||
protected Column[]? columns;
|
||||
private string constraints = "";
|
||||
private string[] post_statements = {};
|
||||
private string[] create_statements = {};
|
||||
internal Column[]? fts_columns;
|
||||
|
||||
public Table(Database db, string name) {
|
||||
this.db = db;
|
||||
|
@ -17,6 +19,37 @@ public class Table {
|
|||
public void init(Column[] columns, string constraints = "") {
|
||||
this.columns = columns;
|
||||
this.constraints = constraints;
|
||||
|
||||
foreach(Column c in columns) {
|
||||
c.table = this;
|
||||
}
|
||||
}
|
||||
|
||||
public void fts(Column[] columns) {
|
||||
if (fts_columns != null) error("Only one FTS index may be used per table.");
|
||||
fts_columns = columns;
|
||||
string cs = "";
|
||||
string cnames = "";
|
||||
string cnews = "";
|
||||
foreach (Column c in columns) {
|
||||
cs += @", $(c.to_column_definition())";
|
||||
cnames += @", $(c.name)";
|
||||
cnews += @", new.$(c.name)";
|
||||
}
|
||||
add_create_statement(@"CREATE VIRTUAL TABLE IF NOT EXISTS _fts_$name USING fts4(tokenize=unicode61, content=\"$name\"$cs)");
|
||||
add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_bu_$(name) BEFORE UPDATE ON $name BEGIN DELETE FROM _fts_$name WHERE docid=old.rowid; END");
|
||||
add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_bd_$(name) BEFORE DELETE ON $name BEGIN DELETE FROM _fts_$name WHERE docid=old.rowid; END");
|
||||
add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_au_$(name) AFTER UPDATE ON $name BEGIN INSERT INTO _fts_$name(docid$cnames) VALUES(new.rowid$cnews); END");
|
||||
add_post_statement(@"CREATE TRIGGER IF NOT EXISTS _fts_ai_$(name) AFTER INSERT ON $name BEGIN INSERT INTO _fts_$name(docid$cnames) VALUES(new.rowid$cnews); END");
|
||||
}
|
||||
|
||||
public void fts_rebuild() {
|
||||
if (fts_columns == null) error("FTS not available on this table.");
|
||||
try {
|
||||
db.exec(@"INSERT INTO _fts_$name(_fts_$name) VALUES('rebuild');");
|
||||
} catch (Error e) {
|
||||
error("Qlite Error: Rebuilding FTS index");
|
||||
}
|
||||
}
|
||||
|
||||
public void unique(Column[] columns, string? on_conflict = null) {
|
||||
|
@ -37,6 +70,10 @@ public class Table {
|
|||
post_statements += stmt;
|
||||
}
|
||||
|
||||
public void add_create_statement(string stmt) {
|
||||
create_statements += stmt;
|
||||
}
|
||||
|
||||
public void index(string index_name, Column[] columns, bool unique = false) {
|
||||
string stmt = @"CREATE $(unique ? "UNIQUE" : "") INDEX IF NOT EXISTS $index_name ON $name (";
|
||||
bool first = true;
|
||||
|
@ -58,6 +95,15 @@ public class Table {
|
|||
return db.select(columns).from(this);
|
||||
}
|
||||
|
||||
private MatchQueryBuilder match_query() {
|
||||
ensure_init();
|
||||
return db.match_query(this);
|
||||
}
|
||||
|
||||
public MatchQueryBuilder match(Column<string> column, string query) {
|
||||
return match_query().match(column, query);
|
||||
}
|
||||
|
||||
public InsertBuilder insert() {
|
||||
ensure_init();
|
||||
return db.insert().into(this);
|
||||
|
@ -98,7 +144,7 @@ public class Table {
|
|||
for (int i = 0; i < columns.length; i++) {
|
||||
Column c = columns[i];
|
||||
if (c.min_version <= version && c.max_version >= version) {
|
||||
sql += @"$(i > 0 ? "," : "") $c";
|
||||
sql += @"$(i > 0 ? "," : "") $(c.to_column_definition())";
|
||||
}
|
||||
}
|
||||
sql += @"$constraints)";
|
||||
|
@ -107,6 +153,13 @@ public class Table {
|
|||
} catch (Error e) {
|
||||
error("Qlite Error: Create table at version");
|
||||
}
|
||||
foreach (string stmt in create_statements) {
|
||||
try {
|
||||
db.exec(stmt);
|
||||
} catch (Error e) {
|
||||
error("Qlite Error: Create table at version");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void add_columns_for_version(long old_version, long new_version) {
|
||||
|
@ -114,7 +167,7 @@ public class Table {
|
|||
foreach (Column c in columns) {
|
||||
if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version) {
|
||||
try {
|
||||
db.exec(@"ALTER TABLE $name ADD COLUMN $c");
|
||||
db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())");
|
||||
} catch (Error e) {
|
||||
error("Qlite Error: Add columns for version");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue