diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 1d32fcd1..80af3d3b 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -38,9 +38,9 @@ public interface Application : GLib.Application { MucManager.start(stream_interactor); AvatarManager.start(stream_interactor, db); RosterManager.start(stream_interactor, db); - ChatInteraction.start(stream_interactor); FileManager.start(stream_interactor, db); ContentItemStore.start(stream_interactor, db); + ChatInteraction.start(stream_interactor); NotificationEvents.start(stream_interactor); SearchProcessor.start(stream_interactor, db); Register.start(stream_interactor, db); diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala index 47ebd5d8..800a28a2 100644 --- a/libdino/src/entity/conversation.vala +++ b/libdino/src/entity/conversation.vala @@ -34,6 +34,7 @@ public class Conversation : Object { } public Encryption encryption { get; set; default = Encryption.NONE; } public Message? read_up_to { get; set; } + public int read_up_to_item { get; set; default=-1; } public enum NotifySetting { DEFAULT, ON, OFF, HIGHLIGHT } public NotifySetting notify_setting { get; set; default = NotifySetting.DEFAULT; } @@ -67,6 +68,7 @@ public class Conversation : Object { encryption = (Encryption) row[db.conversation.encryption]; int? read_up_to = row[db.conversation.read_up_to]; if (read_up_to != null) this.read_up_to = db.get_message_by_id(read_up_to); + read_up_to_item = row[db.conversation.read_up_to_item]; notify_setting = (NotifySetting) row[db.conversation.notification]; send_typing = (Setting) row[db.conversation.send_typing]; send_marker = (Setting) row[db.conversation.send_marker]; @@ -88,6 +90,9 @@ public class Conversation : Object { if (read_up_to != null) { insert.value(db.conversation.read_up_to, read_up_to.id); } + if (read_up_to_item != -1) { + insert.value(db.conversation.read_up_to_item, read_up_to_item); + } if (nickname != null) { insert.value(db.conversation.resource, nickname); } @@ -161,6 +166,13 @@ public class Conversation : Object { update.set_null(db.conversation.read_up_to); } break; + case "read-up-to-item": + if (read_up_to_item != -1) { + update.set(db.conversation.read_up_to_item, read_up_to_item); + } else { + update.set_null(db.conversation.read_up_to_item); + } + break; case "nickname": update.set(db.conversation.resource, nickname); break; case "active": diff --git a/libdino/src/service/chat_interaction.vala b/libdino/src/service/chat_interaction.vala index edfc3913..c832aeca 100644 --- a/libdino/src/service/chat_interaction.vala +++ b/libdino/src/service/chat_interaction.vala @@ -29,44 +29,14 @@ public class ChatInteraction : StreamInteractionModule, Object { Timeout.add_seconds(30, update_interactions); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(stream_interactor)); stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(on_message_sent); + stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(new_item); } public bool has_unread(Conversation conversation) { ContentItem? last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); if (last_content_item == null) return false; - MessageItem? message_item = last_content_item as MessageItem; - if (message_item != null) { - Message last_message = message_item.message; - - // We are the message sender - if (last_message.from.equals_bare(conversation.account.bare_jid)) return false; - // We read up to the message - if (conversation.read_up_to != null && last_message.equals(conversation.read_up_to)) return false; - - return true; - } - - FileItem? file_item = last_content_item as FileItem; - if (file_item != null) { - FileTransfer file_transfer = file_item.file_transfer; - - // We are the file sender - if (file_transfer.from.equals_bare(conversation.account.bare_jid)) return false; - - if (file_transfer.provider == 0) { - // HTTP file transfer: Check if the associated message is the last one - if (file_transfer.info == null) return false; - Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation); - if (message == null) return false; - if (message.equals(conversation.read_up_to)) return false; - } - if (file_transfer.provider == 1) { - if (file_transfer.state == FileTransfer.State.COMPLETE) return false; - } - return true; - } - return false; + return last_content_item.id != conversation.read_up_to_item; } public bool is_active_focus(Conversation? conversation = null) { @@ -106,24 +76,58 @@ public class ChatInteraction : StreamInteractionModule, Object { on_conversation_focused(conversation); } + private void new_item(ContentItem item, Conversation conversation) { + bool mark_read = is_active_focus(conversation); + + if (!mark_read) { + MessageItem? message_item = item as MessageItem; + if (message_item != null) { + if (message_item.message.direction == Message.DIRECTION_SENT) { + mark_read = true; + } + } + if (message_item == null) { + FileItem? file_item = item as FileItem; + if (file_item != null) { + if (file_item.file_transfer.direction == FileTransfer.DIRECTION_SENT) { + mark_read = true; + } + } + } + } + if (mark_read) { + ContentItem? read_up_to = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item); + if (read_up_to != null) { + if (read_up_to.compare(item) < 0) { + conversation.read_up_to_item = item.id; + } + } else { + conversation.read_up_to_item = item.id; + } + } + } + private void on_message_sent(Entities.Message message, Conversation conversation) { last_input_interaction.unset(conversation); last_interface_interaction.unset(conversation); - conversation.read_up_to = message; } private void on_conversation_focused(Conversation? conversation) { focus_in = true; if (conversation == null) return; - focused_in(selected_conversation); + focused_in(conversation); check_send_read(); - selected_conversation.read_up_to = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(conversation); + + ContentItem? latest_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); + if (latest_item != null) { + conversation.read_up_to_item = latest_item.id; + } } private void on_conversation_unfocused(Conversation? conversation) { focus_in = false; if (conversation == null) return; - focused_out(selected_conversation); + focused_out(conversation); if (last_input_interaction.has_key(conversation)) { send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED); last_input_interaction.unset(conversation); @@ -133,8 +137,7 @@ public class ChatInteraction : StreamInteractionModule, Object { private void check_send_read() { if (selected_conversation == null) return; Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(selected_conversation); - if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED && !message.equals(selected_conversation.read_up_to)) { - selected_conversation.read_up_to = message; + if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED) { send_chat_marker(message, null, selected_conversation, Xep.ChatMarkers.MARKER_DISPLAYED); } } @@ -163,7 +166,7 @@ public class ChatInteraction : StreamInteractionModule, Object { private class ReceivedMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ "DEDUPLICATE", "FILTER_EMPTY" }; + public string[] after_actions_const = new string[]{ "DEDUPLICATE", "FILTER_EMPTY", "STORE_CONTENT_ITEM" }; public override string action_group { get { return "OTHER_NODES"; } } public override string[] after_actions { get { return after_actions_const; } } @@ -183,7 +186,6 @@ public class ChatInteraction : StreamInteractionModule, Object { if (message.direction == Entities.Message.DIRECTION_SENT) return false; if (outer.is_active_focus(conversation)) { outer.check_send_read(); - conversation.read_up_to = message; outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_DISPLAYED); } else { outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_RECEIVED); @@ -206,6 +208,9 @@ public class ChatInteraction : StreamInteractionModule, Object { break; case Xep.ChatMarkers.MARKER_DISPLAYED: if (conversation.get_send_marker_setting(stream_interactor) == Conversation.Setting.ON) { + if (message.equals(conversation.read_up_to)) return; + conversation.read_up_to = message; + if (message.type_ == Message.Type.GROUPCHAT || message.type_ == Message.Type.GROUPCHAT_PM) { if (message.server_id == null) return; stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from.bare_jid, message.server_id, message.get_type_string(), Xep.ChatMarkers.MARKER_DISPLAYED); diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 1ea0275e..640c3fda 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -40,7 +40,7 @@ public class ContentItemStore : StreamInteractionModule, Object { } public Gee.List get_items_from_query(QueryBuilder select, Conversation conversation) { - Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare); + Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare_func); foreach (var row in select) { int provider = row[db.content_item.content_type]; @@ -113,6 +113,15 @@ public class ContentItemStore : StreamInteractionModule, Object { return item.size > 0 ? item[0] : null; } + public ContentItem? get_item_by_id(Conversation conversation, int id) { + QueryBuilder select = db.content_item.select() + .with(db.content_item.id, "=", id); + + Gee.List item = get_items_from_query(select, conversation); + + return item.size > 0 ? item[0] : null; + } + public ContentItem? get_latest(Conversation conversation) { Gee.List items = get_n_latest(conversation, 1); if (items.size > 0) { @@ -246,7 +255,11 @@ public abstract class ContentItem : Object { this.mark = mark; } - public static int compare(ContentItem a, ContentItem b) { + public int compare(ContentItem c) { + return compare_func(this, c); + } + + public static int compare_func(ContentItem a, ContentItem b) { int res = a.sort_time.compare(b.sort_time); if (res == 0) { res = a.display_time.compare(b.display_time); diff --git a/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala index 10fbacd6..1e5fd0e2 100644 --- a/libdino/src/service/counterpart_interaction_manager.vala +++ b/libdino/src/service/counterpart_interaction_manager.vala @@ -150,8 +150,13 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object { } if (message == null) return; // Don't move read marker backwards because we get old info from another client - if (conversation.read_up_to == null || conversation.read_up_to.local_time.compare(message.local_time) > 0) return; + if (conversation.read_up_to != null && conversation.read_up_to.local_time.compare(message.local_time) > 0) return; conversation.read_up_to = message; + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); + ContentItem? read_up_to_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item); + if (read_up_to_item != null && read_up_to_item.sort_time.compare(content_item.sort_time) > 0) return; + conversation.read_up_to_item = content_item.id; } else { // We can't currently handle chat markers in MUCs if (conversation.type_ == Conversation.Type.GROUPCHAT) return; diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 4dfcb5b4..a24822d2 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 14; + private const int VERSION = 15; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -166,13 +166,14 @@ public class Database : Qlite.Database { public Column type_ = new Column.Integer("type"); public Column encryption = new Column.Integer("encryption"); public Column read_up_to = new Column.Integer("read_up_to"); + public Column read_up_to_item = new Column.Integer("read_up_to_item") { not_null=true, default="-1", min_version=15 }; public Column notification = new Column.Integer("notification") { min_version=3 }; public Column send_typing = new Column.Integer("send_typing") { min_version=3 }; public Column send_marker = new Column.Integer("send_marker") { min_version=3 }; internal ConversationTable(Database db) { base(db, "conversation"); - init({id, account_id, jid_id, resource, active, last_active, type_, encryption, read_up_to, notification, send_typing, send_marker}); + init({id, account_id, jid_id, resource, active, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker}); } } @@ -365,6 +366,20 @@ public class Database : Qlite.Database { error("Failed to upgrade to database version 12: %s", e.message); } } + if (oldVersion < 15) { + // Initialize `conversation.read_up_to_item` with the content item id corresponding to the `read_up_to` message. + try { + exec(" + update conversation + set read_up_to_item=ifnull(( + select content_item.id + from content_item + where content_item.foreign_id=conversation.read_up_to and content_type=1) + , -1);"); + } catch (Error e) { + error("Failed to upgrade to database version 15: %s", e.message); + } + } } public ArrayList get_accounts() { diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 00abe517..abeaabc7 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -216,9 +216,10 @@ public class FileManager : StreamInteractionModule, Object { } public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { + if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; Jid relevant_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(file_transfer.from, conversation.account) ?? conversation.counterpart; bool in_roster = stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(conversation.account, relevant_jid) != null; - return file_transfer.direction == FileTransfer.DIRECTION_SENT || in_roster; + return in_roster; } private async FileMeta get_file_meta(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation, FileReceiveData receive_data_) throws FileReceiveError { diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala index eb0b9287..9bedf15b 100644 --- a/libdino/src/service/message_correction.vala +++ b/libdino/src/service/message_correction.vala @@ -47,9 +47,6 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { outstanding_correction_nodes[out_message.stanza_id] = stanza_id; stream_interactor.get_module(MessageStorage.IDENTITY).add_message(out_message, conversation); stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation); - if (conversation.read_up_to != null && conversation.read_up_to.equals(old_message)) { // TODO nicer - conversation.read_up_to = out_message; - } db.message_correction.insert() .value(db.message_correction.message_id, out_message.id) diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala index 3f4e3c6d..5e81ff89 100644 --- a/libdino/src/service/notification_events.vala +++ b/libdino/src/service/notification_events.vala @@ -33,15 +33,9 @@ public class NotificationEvents : StreamInteractionModule, Object { } private void on_content_item_received(ContentItem item, Conversation conversation) { - ContentItem last_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); - bool not_read_up_to = true; - MessageItem message_item = item as MessageItem; - if (message_item != null) { - not_read_up_to = conversation.read_up_to != null && !conversation.read_up_to.equals(message_item.message); - } - if (item.id != last_item.id && not_read_up_to) return; + if (item.id != last_item.id && last_item.id != conversation.read_up_to_item) return; if (!should_notify(item, conversation)) return; if (stream_interactor.get_module(ChatInteraction.IDENTITY).is_active_focus()) return; diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index 09de3c80..a5148a8f 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -98,7 +98,7 @@ public class ConversationSelectorRow : ListBoxRow { stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation); }); image.set_conversation(stream_interactor, conversation); - conversation.notify["read-up-to"].connect(update_read); + conversation.notify["read-up-to-item"].connect(update_read); update_name_label(); content_item_received();