Store last read content item for conversations

fixes #495
This commit is contained in:
fiaxh 2020-04-29 21:31:23 +02:00
parent b5066e0e2f
commit 71be2abb6a
10 changed files with 100 additions and 58 deletions

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
}
public Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare);
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(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<ContentItem> item = get_items_from_query(select, conversation);
return item.size > 0 ? item[0] : null;
}
public ContentItem? get_latest(Conversation conversation) {
Gee.List<ContentItem> 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);

View file

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

View file

@ -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<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@ -166,13 +166,14 @@ public class Database : Qlite.Database {
public Column<int> type_ = new Column.Integer("type");
public Column<int> encryption = new Column.Integer("encryption");
public Column<int> read_up_to = new Column.Integer("read_up_to");
public Column<int> read_up_to_item = new Column.Integer("read_up_to_item") { not_null=true, default="-1", min_version=15 };
public Column<int> notification = new Column.Integer("notification") { min_version=3 };
public Column<int> send_typing = new Column.Integer("send_typing") { min_version=3 };
public Column<int> 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<Account> get_accounts() {

View file

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

View file

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

View file

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

View file

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