aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dino.doap8
-rw-r--r--dino.doap.in8
-rw-r--r--libdino/src/service/chat_interaction.vala121
-rw-r--r--libdino/src/service/content_item_store.vala5
-rw-r--r--libdino/src/service/counterpart_interaction_manager.vala83
-rw-r--r--libdino/src/service/file_manager.vala7
-rw-r--r--libdino/src/service/message_processor.vala24
-rw-r--r--libdino/src/service/module_manager.vala1
-rw-r--r--main/src/ui/conversation_content_view/conversation_view.vala337
-rw-r--r--main/src/ui/conversation_selector/conversation_selector_row.vala3
-rw-r--r--main/src/ui/conversation_view_controller.vala10
-rw-r--r--xmpp-vala/meson.build1
-rw-r--r--xmpp-vala/src/module/xep/0060_pubsub.vala11
-rw-r--r--xmpp-vala/src/module/xep/0490_message_displayed_synchronization.vala92
14 files changed, 545 insertions, 166 deletions
diff --git a/dino.doap b/dino.doap
index dee0dd2b..1cde3084 100644
--- a/dino.doap
+++ b/dino.doap
@@ -801,5 +801,13 @@
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0490.html" />
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.0.1</xmpp:version>
+ <xmpp:since>0.6</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
</Project>
</rdf:RDF>
diff --git a/dino.doap.in b/dino.doap.in
index e234e44c..33cf8288 100644
--- a/dino.doap.in
+++ b/dino.doap.in
@@ -621,5 +621,13 @@
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0490.html" />
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.0.1</xmpp:version>
+ <xmpp:since>0.6</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
</Project>
</rdf:RDF>
diff --git a/libdino/src/service/chat_interaction.vala b/libdino/src/service/chat_interaction.vala
index 1254a574..9c8106a0 100644
--- a/libdino/src/service/chat_interaction.vala
+++ b/libdino/src/service/chat_interaction.vala
@@ -18,6 +18,7 @@ public class ChatInteraction : StreamInteractionModule, Object {
private HashMap<Conversation, DateTime> last_input_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
private HashMap<Conversation, DateTime> last_interface_interaction = new HashMap<Conversation, DateTime>(Conversation.hash_func, Conversation.equals_func);
private bool focus_in = false;
+ private bool scrolled_down = true;
public static void start(StreamInteractor stream_interactor) {
ChatInteraction m = new ChatInteraction(stream_interactor);
@@ -66,6 +67,14 @@ public class ChatInteraction : StreamInteractionModule, Object {
on_conversation_unfocused(conversation);
}
+ public void on_scrolled_down_changed(bool scrolled_down, Conversation? conversation) {
+ if (scrolled_down == this.scrolled_down) return;
+ this.scrolled_down = scrolled_down;
+ if (this.scrolled_down && focus_in && selected_conversation == conversation) {
+ mark_latest_message_displayed();
+ }
+ }
+
public void on_message_entered(Conversation? conversation) {
if (!last_input_interaction.has_key(conversation)) {
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING);
@@ -84,25 +93,22 @@ public class ChatInteraction : StreamInteractionModule, Object {
public void on_conversation_selected(Conversation conversation) {
on_conversation_unfocused(selected_conversation);
selected_conversation = conversation;
+ scrolled_down = false;
on_conversation_focused(conversation);
}
private void new_item(ContentItem item, Conversation conversation) {
- bool mark_read = is_active_focus(conversation);
+ bool mark_read = is_active_focus(conversation) && scrolled_down;
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 && 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 (file_item != null && file_item.file_transfer.direction == FileTransfer.DIRECTION_SENT) {
+ mark_read = true;
}
}
}
@@ -127,12 +133,8 @@ public class ChatInteraction : StreamInteractionModule, Object {
focus_in = true;
if (conversation == null) return;
focused_in(conversation);
- check_send_read();
- ContentItem? latest_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation);
- if (latest_item != null) {
- conversation.read_up_to_item = latest_item.id;
- }
+ if (scrolled_down) mark_latest_message_displayed();
}
private void on_conversation_unfocused(Conversation? conversation) {
@@ -145,12 +147,15 @@ 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) {
- send_chat_marker(message, null, selected_conversation, Xep.ChatMarkers.MARKER_DISPLAYED);
- }
+ private async bool mark_latest_message_displayed() {
+ Conversation conversation = selected_conversation;
+ if (conversation == null) return false;
+ Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(conversation);
+ if (message == null || message.direction == Entities.Message.DIRECTION_SENT) return false;
+ if (message.equals(conversation.read_up_to)) return false;
+
+ bool sent_marker = yield send_and_mark_message_displayed(conversation, message);
+ return sent_marker;
}
private bool update_interactions() {
@@ -195,43 +200,67 @@ public class ChatInteraction : StreamInteractionModule, Object {
// Send chat marker
if (message.direction == Entities.Message.DIRECTION_SENT) return false;
- if (outer.is_active_focus(conversation)) {
- outer.check_send_read();
- outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_DISPLAYED);
+ if (outer.is_active_focus(conversation) && outer.scrolled_down) {
+ yield outer.send_and_mark_message_displayed(conversation, message);
} else {
- outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_RECEIVED);
+ outer.send_received_marker(message, stanza, conversation);
}
return false;
}
}
+ public async bool send_and_mark_message_displayed(Conversation conversation, Entities.Message message) {
+ stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).mark_displayed_up_to_message(conversation, message);
+ bool sent_marker = send_displayed_marker(message, conversation);
+ if (!sent_marker) {
+ sent_marker = yield send_message_displayed_sync(message, conversation);
+ }
+ return sent_marker;
+ }
+
+ private bool send_received_marker(Entities.Message message, Xmpp.MessageStanza? stanza, Conversation conversation) {
+ if (stanza == null) return false;
+ if (!Xep.ChatMarkers.Module.requests_marking(stanza)) return false;
+ if (message.type_ == Message.Type.GROUPCHAT) return false;
+ if (message.stanza_id == null) return false;
+ if (message.direction == Entities.Message.DIRECTION_SENT) return false;
+ if (message.equals(conversation.read_up_to)) return false;
+ if (conversation.get_send_marker_setting(stream_interactor) != Conversation.Setting.ON) return false;
- private void send_chat_marker(Entities.Message message, Xmpp.MessageStanza? stanza, Conversation conversation, string marker) {
XmppStream? stream = stream_interactor.get_stream(conversation.account);
- if (stream == null) return;
+ if (stream == null) return false;
- switch (marker) {
- case Xep.ChatMarkers.MARKER_RECEIVED:
- if (stanza != null && Xep.ChatMarkers.Module.requests_marking(stanza) && message.type_ != Message.Type.GROUPCHAT) {
- if (message.stanza_id == null) return;
- stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_RECEIVED);
- }
- 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;
+ stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_RECEIVED);
+ return true;
+ }
- 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);
- } else {
- if (message.stanza_id == null) return;
- stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_DISPLAYED);
- }
- }
- break;
+ private bool send_displayed_marker(Entities.Message message, Conversation conversation) {
+ if (message.direction == Entities.Message.DIRECTION_SENT) return false;
+ if (conversation.get_send_marker_setting(stream_interactor) != Conversation.Setting.ON) return false;
+
+ XmppStream? stream = stream_interactor.get_stream(conversation.account);
+ if (stream == null) return false;
+
+ if (message.type_.is_muc_semantic() && message.server_id != null) {
+ 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);
+ return true;
+ } else if (!message.type_.is_muc_semantic() && message.stanza_id != null) {
+ stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_DISPLAYED);
+ return true;
}
+
+ return false;
+ }
+
+ private async bool send_message_displayed_sync(Entities.Message message, Conversation conversation) {
+ if (message.direction == Entities.Message.DIRECTION_SENT) return false;
+ if (message.server_id == null) return false;
+
+ XmppStream? stream = stream_interactor.get_stream(conversation.account);
+ if (stream == null) return false;
+
+ Jid server_id_jid = conversation.type_ == Conversation.Type.GROUPCHAT ? conversation.counterpart : conversation.account.bare_jid;
+ return yield stream.get_module(Xep.MessageDisplayedSynchronization.Module.IDENTITY).update_message_displayed(stream, conversation.counterpart, server_id_jid, message.server_id);
}
private void send_delivery_receipt(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
index 37c82eb7..d74f9b05 100644
--- a/libdino/src/service/content_item_store.vala
+++ b/libdino/src/service/content_item_store.vala
@@ -76,10 +76,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
case 2:
FileTransfer? file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(foreign_id, conversation);
if (file_transfer != null) {
- Message? message = null;
- if (file_transfer.provider == 0 && file_transfer.info != null) {
- message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation);
- }
+ Message? message = stream_interactor.get_module(FileManager.IDENTITY).get_message_for_file_transfer(file_transfer, conversation);
var file_item = new FileItem(file_transfer, conversation, id, message);
return file_item;
}
diff --git a/libdino/src/service/counterpart_interaction_manager.vala b/libdino/src/service/counterpart_interaction_manager.vala
index 23db5762..f60cfb20 100644
--- a/libdino/src/service/counterpart_interaction_manager.vala
+++ b/libdino/src/service/counterpart_interaction_manager.vala
@@ -73,6 +73,9 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
stream_interactor.module_manager.get_module(account, Xep.ChatStateNotifications.Module.IDENTITY).chat_state_received.connect((stream, jid, state, stanza) => {
on_chat_state_received.begin(account, jid, state, stanza);
});
+ stream_interactor.module_manager.get_module(account, Xep.MessageDisplayedSynchronization.Module.IDENTITY).on_message_displayed.connect((conversation_jid, stanza_id_by_jid, stanza_id) => {
+ on_message_displayed_received(account, conversation_jid, stanza_id_by_jid, stanza_id);
+ });
}
private void clear_chat_state(Conversation conversation, Jid jid) {
@@ -120,17 +123,69 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
private async void on_chat_marker_received(Account account, Jid jid, string marker, string stanza_id, MessageStanza message_stanza) {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(message_stanza.from, message_stanza.to, account, message_stanza.type_);
if (conversation == null) return;
- handle_chat_marker(conversation, jid, marker, stanza_id);
+ handle_chat_marker(conversation, jid, marker, stanza_id, message_stanza);
+ }
+
+ private void on_message_displayed_received(Account account, Jid conversation_jid, Jid stanza_id_by_jid, string stanza_id) {
+ Conversation? conversation = null;
+ if (stanza_id_by_jid.equals(conversation_jid) && conversation_jid.is_bare()) {
+ conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(conversation_jid, account, Conversation.Type.GROUPCHAT);
+ } else if (conversation_jid.resourcepart != null && stanza_id_by_jid.equals(account.bare_jid)) {
+ conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(conversation_jid, account, Conversation.Type.GROUPCHAT_PM);
+ } else if (stanza_id_by_jid.equals(account.bare_jid)) {
+ conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(conversation_jid, account, Conversation.Type.CHAT);
+ }
+ if (conversation == null) return;
+
+ mark_displayed_up_to_stanza_id(conversation, stanza_id, true);
+ }
+
+ public bool mark_displayed_up_to_stanza_id(Conversation conversation, string stanza_id, bool server_id_only) {
+ Entities.Message? message = null;
+ if (server_id_only) {
+ message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(stanza_id, conversation);
+ } else if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
+ message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(stanza_id, conversation);
+ // Outdated clients might use the message id. Or in MUCs that don't send server ids.
+ if (message == null) {
+ message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
+ }
+ } else {
+ message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
+ }
+ if (message == null) return false;
+ return mark_displayed_up_to_message(conversation, message);
+ }
+
+ public bool mark_displayed_up_to_message(Conversation conversation, Message message) {
+ // 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 false;
+ conversation.read_up_to = message;
+
+ ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_message(conversation, message);
+ if (content_item == null) return true;
+ return mark_displayed_up_to_content_item(conversation, content_item);
+ }
+
+ public bool mark_displayed_up_to_content_item(Conversation conversation, ContentItem content_item) {
+ 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.compare(content_item) > 0) return false;
+ conversation.read_up_to_item = content_item.id;
+ return true;
}
- private void handle_chat_marker(Conversation conversation, Jid jid, string marker, string stanza_id) {
+ private void handle_chat_marker(Conversation conversation, Jid jid, string marker, string stanza_id, MessageStanza? message_stanza = null) {
// Check if the marker comes from ourselves (own jid or our jid in a MUC)
bool own_marker = false;
if (conversation.type_ == Conversation.Type.CHAT) {
own_marker = conversation.account.bare_jid.to_string() == jid.bare_jid.to_string();
} else {
Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(jid.bare_jid, conversation.account);
+ string own_occupant_id = stream_interactor.get_module(MucManager.IDENTITY).get_own_occupant_id(conversation.account, jid.bare_jid);
+ string occupant_id = Xep.OccupantIds.get_occupant_id(message_stanza.stanza);
if (own_muc_jid != null && own_muc_jid.equals(jid)) {
+ // Don't allow groupchat displayed marker without matching occupant id
+ if (conversation.type_ == Conversation.Type.GROUPCHAT && (occupant_id == null || own_occupant_id == occupant_id)) return;
own_marker = true;
}
}
@@ -138,27 +193,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
if (own_marker) {
// If we received a display marker from ourselves (other device), set the conversation read up to that message.
if (marker != Xep.ChatMarkers.MARKER_DISPLAYED && marker != Xep.ChatMarkers.MARKER_ACKNOWLEDGED) return;
- Entities.Message? message = null;
- if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
- message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(stanza_id, conversation);
- // Outdated clients might use the message id. Or in MUCs that don't send server ids.
- if (message == null) {
- message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
- }
- } else {
- message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
- }
- 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;
- conversation.read_up_to = message;
-
- // TODO: This only marks messages as read, not http file transfers.
- ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
- if (content_item == null) return;
- 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.compare(content_item) > 0) return;
- conversation.read_up_to_item = content_item.id;
+ mark_displayed_up_to_stanza_id(conversation, stanza_id, false);
} else {
// We can't currently handle chat markers in MUCs
if (conversation.type_ == Conversation.Type.GROUPCHAT) return;
@@ -203,7 +238,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
private void on_receipt_received(Account account, Jid jid, string id, MessageStanza stanza) {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(stanza.from, stanza.to, account, stanza.type_);
if (conversation == null) return;
- handle_chat_marker(conversation, jid,Xep.ChatMarkers.MARKER_RECEIVED, id);
+ handle_chat_marker(conversation, jid,Xep.ChatMarkers.MARKER_RECEIVED, id, stanza);
}
}
diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala
index 8c196dd7..5e59aee1 100644
--- a/libdino/src/service/file_manager.vala
+++ b/libdino/src/service/file_manager.vala
@@ -420,6 +420,13 @@ public class FileManager : StreamInteractionModule, Object {
throw new FileSendError.SAVE_FAILED("Saving file error: %s".printf(e.message));
}
}
+
+ public Message? get_message_for_file_transfer(FileTransfer file_transfer, Conversation conversation) {
+ if ((file_transfer.provider == 0 || file_transfer.provider == 2) && file_transfer.info != null) {
+ return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation);
+ }
+ return null;
+ }
}
public errordomain FileSendError {
diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala
index 676daa72..7201085e 100644
--- a/libdino/src/service/message_processor.vala
+++ b/libdino/src/service/message_processor.vala
@@ -39,6 +39,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
received_pipeline.connect(new StoreMessageListener(this, stream_interactor));
received_pipeline.connect(new StoreContentItemListener(stream_interactor));
received_pipeline.connect(new MarkupListener(stream_interactor));
+ received_pipeline.connect(new PreDisplayedHandler(stream_interactor));
stream_interactor.account_added.connect(on_account_added);
@@ -379,6 +380,29 @@ public class MessageProcessor : StreamInteractionModule, Object {
}
}
+ private class PreDisplayedHandler : MessageListener {
+ public string[] after_actions_const = new string[]{ "STORE_CONTENT_ITEM" };
+ public override string action_group { get { return "DISPLAYED_HANDLING"; } }
+ public override string[] after_actions { get { return after_actions_const; } }
+
+ private StreamInteractor stream_interactor;
+
+ public PreDisplayedHandler(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ var flag = stream_interactor.get_stream(conversation.account).get_flag(MessageDisplayedSynchronization.Flag.IDENTITY);
+ if (flag != null && message.server_id != null) {
+ Jid stanza_id_by = message.type_ == Message.Type.GROUPCHAT ? conversation.counterpart.bare_jid : conversation.account.bare_jid;
+ if (flag.is_displayed(conversation.counterpart, stanza_id_by, message.server_id)) {
+ stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).mark_displayed_up_to_message(conversation, message);
+ }
+ }
+ return false;
+ }
+ }
+
public Entities.Message create_out_message(string? text, Conversation conversation) {
Entities.Message message = new Entities.Message(text);
message.type_ = Util.get_message_type_for_conversation(conversation);
diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala
index 127f2d93..f83ce425 100644
--- a/libdino/src/service/module_manager.vala
+++ b/libdino/src/service/module_manager.vala
@@ -100,6 +100,7 @@ public class ModuleManager {
module_map[account].add(new Xep.Muji.Module());
module_map[account].add(new Xep.CallInvites.Module());
module_map[account].add(new Xep.Coin.Module());
+ module_map[account].add(new Xep.MessageDisplayedSynchronization.Module());
initialize_account_modules(account, module_map[account]);
}
}
diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala
index 97e3b01d..e8015698 100644
--- a/main/src/ui/conversation_content_view/conversation_view.vala
+++ b/main/src/ui/conversation_content_view/conversation_view.vala
@@ -9,6 +9,8 @@ namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/view.ui")]
public class ConversationView : Widget, Plugins.ConversationItemCollection, Plugins.NotificationCollection {
+ private const int AROUND_MESSAGE_WINDOW = 40;
+ private const int EXPAND_MESSAGE_WINDOW = 20;
private const int MESSAGE_MENU_BOX_OFFSET = -20;
public Conversation? conversation { get; private set; }
@@ -32,14 +34,24 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
private ContentProvider content_populator;
private SubscriptionNotitication subscription_notification;
+ private bool scroll_active = false;
+ private Plugins.MetaConversationItem scroll_target_item;
+ private bool should_scroll_after_item = false;
+ private bool should_scroll_with_animation = false;
+ private bool should_highlight_after_scroll = false;
+ private bool programmatic_scrolling = false;
+
+ private uint mark_lowest_message_read_timeout = 0;
+
private double? was_value;
private double? was_upper;
private double? was_page_size;
private Mutex reloading_mutex = Mutex();
private bool firstLoad = true;
- private bool at_current_content = true;
- private bool reload_messages = true;
+ public bool at_current_content {
+ get; private set; default = true;
+ }
Widget currently_highlighted = null;
ContentMetaItem? current_meta_item = null;
double last_y = -1;
@@ -271,7 +283,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
}
}
- public void initialize_for_conversation(Conversation? conversation) {
+ public void initialize_for_conversation(Conversation? conversation, bool go_to_end = false) {
// Workaround for rendering issues
if (firstLoad) {
main.visible = false;
@@ -281,65 +293,142 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
});
firstLoad = false;
}
- if (conversation == this.conversation && at_current_content) {
- // Just make sure we are scrolled down
- if (scrolled.vadjustment.value != scrolled.vadjustment.upper) {
- scroll_animation(scrolled.vadjustment.upper).play();
+ if (conversation == this.conversation) {
+ if (go_to_end) {
+ if (at_current_content) {
+ ensure_scroll_to_end(true);
+ return;
+ }
+ } else {
+ // Just make sure we are scrolled to unread line
+ Plugins.MetaConversationItem scroll_target_item = content_items.first_match((it) => it.content_item.id == conversation.read_up_to_item);
+ if (scroll_target_item != null) {
+ ensure_scroll_to_item(scroll_target_item, true, true, false);
+ return;
+ }
}
- return;
}
+ start_load_new_conversation();
+ Idle.add_once(() => {
+ initialize_for_conversation_(conversation);
+ ContentItem? content_item = null;
+ if (!go_to_end && conversation.read_up_to_item > 0) {
+ content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item);
+ }
+ if (content_item != null) {
+ ContentMetaItem meta_item = populate_messages_for(content_item);
+ ensure_scroll_to_item(meta_item, true, false, false);
+ } else {
+ display_latest();
+ at_current_content = true;
+ // Scroll to end
+ ensure_scroll_to_end(false);
+ }
+ Idle.add_once(finish_load_new_conversation);
+ });
+ }
+
+ private void start_load_new_conversation() {
+ if (!reloading_mutex.trylock()) {
+ reloading_mutex.unlock();
+ reloading_mutex.lock();
+ }
+ scrolled.vadjustment.freeze_notify();
+ main.opacity = 0;
clear();
- initialize_for_conversation_(conversation);
- display_latest();
- at_current_content = true;
- // Scroll to end
- scrolled.vadjustment.value = scrolled.vadjustment.upper;
}
- private void scroll_and_highlight_item(Plugins.MetaConversationItem target, uint duration = 500) {
- Widget widget = null;
- int h = 0;
- foreach (Plugins.MetaConversationItem item in meta_items) {
- widget = widgets[item];
- if (target == item) {
- break;
+ private void finish_load_new_conversation() {
+ programmatic_scrolling = true;
+ scrolled.vadjustment.thaw_notify();
+ programmatic_scrolling = false;
+ main.opacity = 1;
+ reloading_mutex.trylock();
+ reloading_mutex.unlock();
+ start_mark_visible_messages_read();
+ }
+
+ private void ensure_scroll_to_item(Plugins.MetaConversationItem target, bool after, bool animate, bool highlight) {
+ scroll_active = true;
+ scroll_target_item = target;
+ should_scroll_after_item = after;
+ should_highlight_after_scroll = highlight;
+ should_scroll_with_animation = animate;
+ apply_scroll();
+ }
+
+ private void ensure_scroll_to_end(bool animate) {
+ scroll_active = true;
+ scroll_target_item = null;
+ should_scroll_after_item = true;
+ should_highlight_after_scroll = false;
+ should_scroll_with_animation = animate;
+ apply_scroll();
+ }
+
+ private void apply_scroll() {
+ if (!scroll_active) return;
+ double target_height = scrolled.vadjustment.value;
+ Widget? widget = null;
+ if (scroll_target_item != null) {
+ widget = widgets[scroll_target_item];
+ if (widget == null || !widget.get_realized() || !widget.get_mapped()) {
+ warning("No widget when trying to scroll, or widget not realized/mapped");
+ return;
+ }
+ float h = 0;
+ Graphene.Rect bounds;
+ bool got_bounds = widget.compute_bounds(main, out bounds);
+ if (!got_bounds) {
+ warning("No bounds when trying to scroll");
+ return;
}
- h += widget.get_allocated_height();
+ h = bounds.origin.y;
+ if (should_scroll_after_item) h += bounds.size.height;
+ target_height = h - scrolled.vadjustment.page_size * 1 / 3;
+ } else {
+ target_height = scrolled.vadjustment.upper - scrolled.vadjustment.page_size;
}
- if (widget != widgets[target]) {
- warning("Target item widget not reached");
- return;
+ programmatic_scrolling = true;
+ if (!should_scroll_with_animation) {
+ scrolled.vadjustment.value = target_height;
+ if (should_highlight_after_scroll && widget != null) {
+ highlight_once(widget);
+ }
+ programmatic_scrolling = false;
+ } else {
+ Adw.Animation animation = scroll_animation(target_height, 500);
+ if (should_highlight_after_scroll && widget != null) {
+ animation.done.connect(() => {
+ highlight_once(widget);
+ });
+ }
+ animation.done.connect(() => {
+ programmatic_scrolling = false;
+ should_scroll_with_animation = false;
+ });
+ animation.play();
}
- double target_height = h - scrolled.vadjustment.page_size * 1/3;
- Adw.Animation animation = scroll_animation(target_height);
- animation.done.connect(() => {
+ }
+
+ private void highlight_once(Widget widget) {
+ widget.remove_css_class("highlight-once");
+ widget.add_css_class("highlight-once");
+ Timeout.add(5000, () => {
widget.remove_css_class("highlight-once");
- widget.add_css_class("highlight-once");
- Timeout.add(5000, () => {
- widget.remove_css_class("highlight-once");
- return false;
- });
+ should_highlight_after_scroll = false;
+ return false;
});
- animation.play();
}
- private Adw.Animation scroll_animation(double target) {
- return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500,
+ private Adw.Animation scroll_animation(double target, uint duration = 500) {
+ return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, duration,
new Adw.PropertyAnimationTarget(scrolled.vadjustment, "value")
);
}
- public void initialize_around_message(Conversation conversation, ContentItem content_item) {
- if (conversation == this.conversation) {
- ContentMetaItem? matching_item = content_items.first_match(it => it.content_item.id == content_item.id);
- if (matching_item != null) {
- scroll_and_highlight_item(matching_item);
- return;
- }
- }
- clear();
- initialize_for_conversation_(conversation);
- Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
+ private ContentMetaItem populate_messages_for(ContentItem content_item) {
+ Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, AROUND_MESSAGE_WINDOW);
foreach (ContentMetaItem item in before_items) {
do_insert_item(item);
}
@@ -348,20 +437,28 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
content_items.add(meta_item);
meta_items.add(meta_item);
- Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40);
+ Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, AROUND_MESSAGE_WINDOW);
foreach (ContentMetaItem item in after_items) {
do_insert_item(item);
}
- if (after_items.size == 40) {
- at_current_content = false;
- }
+ at_current_content = after_items.size < AROUND_MESSAGE_WINDOW;
+ return meta_item;
+ }
- // Compute where to jump to for centered message, jump, highlight.
- reload_messages = false;
- Timeout.add(700, () => {
- scroll_and_highlight_item(meta_item, 300);
- reload_messages = true;
- return false;
+ public void initialize_around_message(Conversation conversation, ContentItem content_item) {
+ if (conversation == this.conversation) {
+ ContentMetaItem? matching_item = content_items.first_match(it => it.content_item.id == content_item.id);
+ if (matching_item != null) {
+ ensure_scroll_to_item(matching_item, false, true, true);
+ return;
+ }
+ }
+ start_load_new_conversation();
+ Idle.add_once(() => {
+ initialize_for_conversation_(conversation);
+ ContentMetaItem meta_item = populate_messages_for(content_item);
+ ensure_scroll_to_item(meta_item, false, false, true);
+ Idle.add_once(finish_load_new_conversation);
});
}
@@ -388,21 +485,18 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK4);
}
+ foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
+ populator.init(conversation, this, Plugins.WidgetType.GTK4);
+ }
content_populator.init(this, conversation, Plugins.WidgetType.GTK4);
subscription_notification.init(conversation, this);
}
private void display_latest() {
- Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40);
+ Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, AROUND_MESSAGE_WINDOW);
foreach (ContentMetaItem item in items) {
do_insert_item(item);
}
-
- Application app = GLib.Application.get_default() as Application;
- foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
- populator.init(conversation, this, Plugins.WidgetType.GTK4);
- }
- Idle.add(() => { on_value_notify(); return false; });
}
public void insert_item(Plugins.MetaConversationItem item) {
@@ -624,16 +718,27 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
}
private void on_upper_notify() {
- 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) {
- Idle.add(() => {
- // If we do this directly without Idle.add, scrolling down doesn't work properly
- scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
- return false;
- });
+ if (scroll_active) {
+ // Still scrolling, ignore
+ Idle.add_once(apply_scroll);
+ return;
+ }
+ if (was_upper != null) {
+ if (scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
+ if (at_current_content) {
+ Idle.add(() => {
+ // If we do this directly without Idle.add, scrolling down doesn't work properly
+ programmatic_scrolling = true;
+ scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
+ programmatic_scrolling = false;
+ return false;
+ });
+ }
+ } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
+ programmatic_scrolling = true;
+ scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
+ programmatic_scrolling = false;
}
- } 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;
@@ -642,21 +747,82 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
reloading_mutex.unlock();
}
+ private void start_mark_visible_messages_read() {
+ if (mark_lowest_message_read_timeout == 0) {
+ mark_lowest_message_read_timeout = Timeout.add_once(1000, mark_visible_messages_read);
+ }
+ }
+
+ private void mark_visible_messages_read() {
+ double y;
+ scrolled.translate_coordinates(main, 0, scrolled.get_allocated_height(), null, out y);
+
+ // Get last item fully displayed
+ int h = 0;
+ ContentMetaItem? displayed_item = null;
+ foreach (Plugins.MetaConversationItem item in meta_items) {
+ Widget widget = widgets[item];
+ h += widget.get_allocated_height() + widget.margin_top + widget.margin_bottom;
+ if (h >= y) {
+ break;
+ }
+ if (item is ContentMetaItem) {
+ displayed_item = (ContentMetaItem) item;
+ }
+ }
+ if (displayed_item != null && displayed_item.content_item.id != conversation.read_up_to_item) {
+ mark_read.begin(displayed_item, (_, res) => {
+ mark_read.end(res);
+ });
+ }
+ mark_lowest_message_read_timeout = 0;
+ }
+
+ private async void mark_read(ContentMetaItem displayed_item) {
+ if (displayed_item.content_item is MessageItem) {
+ MessageItem message_item = (MessageItem) displayed_item.content_item;
+ yield stream_interactor.get_module(ChatInteraction.IDENTITY).send_and_mark_message_displayed(message_item.conversation, message_item.message);
+ } else if (displayed_item.content_item is FileItem) {
+ FileItem file_item = (FileItem) displayed_item.content_item;
+ Message? message = stream_interactor.get_module(FileManager.IDENTITY).get_message_for_file_transfer(file_item.file_transfer, conversation);
+ if (message != null) {
+ yield stream_interactor.get_module(ChatInteraction.IDENTITY).send_and_mark_message_displayed(file_item.conversation, message);
+ } else {
+ stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).mark_displayed_up_to_content_item(file_item.conversation, file_item);
+ }
+ } else {
+ stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).mark_displayed_up_to_content_item(conversation, displayed_item.content_item);
+ }
+ }
+
private void on_value_notify() {
+ if (!programmatic_scrolling) {
+ scroll_active = false;
+ start_mark_visible_messages_read();
+ }
if (scrolled.vadjustment.value < 400) {
load_earlier_messages();
- } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
+ } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400 && !at_current_content) {
load_later_messages();
}
}
+ private void stop_scroll_fling() {
+ Idle.add_once(() => scrolled.vadjustment.value = scrolled.vadjustment.value);
+ }
+
private void load_earlier_messages() {
was_value = scrolled.vadjustment.value;
if (!reloading_mutex.trylock()) return;
if (content_items.size > 0) {
- Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, ((ContentMetaItem) content_items.first()).content_item, 20);
- foreach (ContentMetaItem item in items) {
- do_insert_item(item);
+ Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, ((ContentMetaItem) content_items.first()).content_item, EXPAND_MESSAGE_WINDOW);
+ if (items.is_empty) {
+ reloading_mutex.unlock();
+ } else {
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ stop_scroll_fling();
}
} else {
reloading_mutex.unlock();
@@ -666,12 +832,15 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
private void load_later_messages() {
if (!reloading_mutex.trylock()) return;
if (content_items.size > 0 && !at_current_content) {
- Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, ((ContentMetaItem) content_items.last()).content_item, 20);
- if (items.size == 0) {
- at_current_content = true;
- }
- foreach (ContentMetaItem item in items) {
- do_insert_item(item);
+ Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, ((ContentMetaItem) content_items.last()).content_item, EXPAND_MESSAGE_WINDOW);
+ at_current_content = items.size < EXPAND_MESSAGE_WINDOW;
+ if (items.is_empty) {
+ reloading_mutex.unlock();
+ } else {
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ stop_scroll_fling();
}
} else {
reloading_mutex.unlock();
@@ -690,6 +859,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
}
private void clear() {
+ if (mark_lowest_message_read_timeout != 0) {
+ Source.remove(mark_lowest_message_read_timeout);
+ mark_lowest_message_read_timeout = 0;
+ }
was_upper = null;
was_page_size = null;
foreach (var item in content_items) {
diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala
index d78038f9..73bc36bd 100644
--- a/main/src/ui/conversation_selector/conversation_selector_row.vala
+++ b/main/src/ui/conversation_selector/conversation_selector_row.vala
@@ -253,9 +253,10 @@ public class ConversationSelectorRow : ListBoxRow {
if (update_read_pending) return;
update_read_pending = true;
Idle.add(() => {
+ bool force = update_read_pending_force;
update_read_pending = false;
update_read_pending_force = false;
- update_read_idle(update_read_pending_force);
+ update_read_idle(force);
return Source.REMOVE;
}, Priority.LOW);
}
diff --git a/main/src/ui/conversation_view_controller.vala b/main/src/ui/conversation_view_controller.vala
index 08f9dd5d..ef13b026 100644
--- a/main/src/ui/conversation_view_controller.vala
+++ b/main/src/ui/conversation_view_controller.vala
@@ -56,12 +56,14 @@ public class ConversationViewController : Object {
// goto-end floating button
var vadjustment = view.conversation_frame.scrolled.vadjustment;
vadjustment.notify["value"].connect(() => {
- bool button_active = vadjustment.value < vadjustment.upper - vadjustment.page_size;
- view.goto_end_revealer.reveal_child = button_active;
- view.goto_end_revealer.visible = button_active;
+ debug_here();
+ bool scrolled_up = vadjustment.value < vadjustment.upper - vadjustment.page_size;
+ view.goto_end_revealer.reveal_child = scrolled_up;
+ view.goto_end_revealer.visible = scrolled_up;
+ stream_interactor.get_module(ChatInteraction.IDENTITY).on_scrolled_down_changed(!scrolled_up && view.conversation_frame.at_current_content, conversation);
});
view.goto_end_button.clicked.connect(() => {
- view.conversation_frame.initialize_for_conversation(conversation);
+ view.conversation_frame.initialize_for_conversation(conversation, true);
});
// Update conversation topic
diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build
index 85bdaadc..3f00ea7b 100644
--- a/xmpp-vala/meson.build
+++ b/xmpp-vala/meson.build
@@ -136,6 +136,7 @@ sources = files(
'src/module/xep/0461_replies.vala',
'src/module/xep/0463_muc_affiliations_versioning.vala',
'src/module/xep/0482_call_invites.vala',
+ 'src/module/xep/0490_message_displayed_synchronization.vala',
'src/file_content_type.vala',
'src/util.vala',
'src/weak_map.vala',
diff --git a/xmpp-vala/src/module/xep/0060_pubsub.vala b/xmpp-vala/src/module/xep/0060_pubsub.vala
index c6395eb9..c9af4263 100644
--- a/xmpp-vala/src/module/xep/0060_pubsub.vala
+++ b/xmpp-vala/src/module/xep/0060_pubsub.vala
@@ -41,9 +41,9 @@ namespace Xmpp.Xep.Pubsub {
retract_listeners.unset(node);
}
- public async Gee.List<StanzaNode>? request_all(XmppStream stream, Jid jid, string node) { // TODO multiple nodes gehen auch
+ public async Gee.List<StanzaNode>? request_all(XmppStream stream, Jid? jid, string node) { // TODO multiple nodes gehen auch
Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(new StanzaNode.build("items", NS_URI).put_attribute("node", node)));
- request_iq.to = jid;
+ if (jid != null) request_iq.to = jid;
Iq.Stanza iq_res = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, request_iq);
@@ -56,9 +56,9 @@ namespace Xmpp.Xep.Pubsub {
}
public delegate void OnResult(XmppStream stream, Jid jid, string? id, StanzaNode? node);
- public void request(XmppStream stream, Jid jid, string node, owned OnResult listener) { // TODO multiple nodes gehen auch
+ public void request(XmppStream stream, Jid? jid, string node, owned OnResult listener) { // TODO multiple nodes gehen auch
Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(new StanzaNode.build("items", NS_URI).put_attribute("node", node)));
- request_iq.to = jid;
+ if (jid != null) request_iq.to = jid;
stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_iq, (stream, iq) => {
StanzaNode event_node = iq.stanza.get_subnode("pubsub", NS_URI);
StanzaNode items_node = event_node != null ? event_node.get_subnode("items", NS_URI) : null;
@@ -100,6 +100,7 @@ namespace Xmpp.Xep.Pubsub {
}
Iq.Stanza iq = new Iq.Stanza.set(pubsub_node);
+ if (jid != null) iq.to = jid;
// If the node was configured differently before, reconfigure it to meet our requirements and try again
Iq.Stanza iq_result = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq);
@@ -222,7 +223,7 @@ namespace Xmpp.Xep.Pubsub {
}
private async bool change_node_config(XmppStream stream, Jid jid, string node, PublishOptions publish_options) {
- DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, node);
+ DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, jid, node);
if (data_form == null) return false;
foreach (DataForms.DataForm.Field field in data_form.fields) {
diff --git a/xmpp-vala/src/module/xep/0490_message_displayed_synchronization.vala b/xmpp-vala/src/module/xep/0490_message_displayed_synchronization.vala
new file mode 100644
index 00000000..38437735
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0490_message_displayed_synchronization.vala
@@ -0,0 +1,92 @@
+using Gee;
+namespace Xmpp.Xep.MessageDisplayedSynchronization {
+ public const string NS_URI = "urn:xmpp:mds:displayed:0";
+ public const string NS_URI_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
+
+ public class Module : XmppStreamModule {
+ public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0490_message_displayed_synchronization");
+
+ public signal void on_message_displayed(Jid conversation_jid, Jid stanza_id_by_jid, string stanza_id);
+
+ private void handle_displayed_node_item(XmppStream stream, string? id, StanzaNode? item) {
+ if (id == null || item == null) return;
+ Jid conversation_jid = Jid.from_string(id);
+ StanzaNode? stanza_id_node = item.get_subnode("stanza-id", UniqueStableStanzaIDs.NS_URI);
+ if (stanza_id_node == null || stanza_id_node.get_attribute("by") == null || stanza_id_node.get_attribute("id") == null) return;
+ Jid stanza_id_by_jid = Jid.from_string(stanza_id_node.get_attribute("by"));
+ string stanza_id = stanza_id_node.get_attribute("id");
+ if (stream.has_flag(Flag.IDENTITY)) {
+ stream.get_flag(Flag.IDENTITY).learn_mds(conversation_jid, stanza_id_by_jid, stanza_id);
+ }
+ on_message_displayed(conversation_jid, stanza_id_by_jid, stanza_id);
+ }
+
+ private void on_pubsub_item(XmppStream stream, Jid jid, string id, StanzaNode? item) {
+ if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) {
+ warning("Received alleged mds item from %s, ignoring", jid.to_string());
+ return;
+ }
+ handle_displayed_node_item(stream, id, item);
+ }
+
+ private void on_initial_presence_sent(XmppStream stream) {
+ var pubsub = stream.get_module(Pubsub.Module.IDENTITY);
+ pubsub.request_all.begin(stream, null, NS_URI, (_, res) => {
+ Gee.List<StanzaNode>? items = pubsub.request_all.end(res);
+ foreach (StanzaNode node_item in items) {
+ handle_displayed_node_item(stream, node_item.get_attribute("id"), node_item.get_subnode("displayed", NS_URI));
+ }
+ });
+ }
+
+ public async bool update_message_displayed(XmppStream stream, Jid conversation_jid, Jid stanza_id_by_jid, string stanza_id) {
+ var pubsub = stream.get_module(Pubsub.Module.IDENTITY);
+ var node = new StanzaNode.build("displayed", NS_URI).add_self_xmlns()
+ .put_node(new StanzaNode.build("stanza-id", UniqueStableStanzaIDs.NS_URI).add_self_xmlns()
+ .put_attribute("by", stanza_id_by_jid.to_string())
+ .put_attribute("id", stanza_id));
+ var options = new Pubsub.PublishOptions()
+ .set_persist_items(true)
+ .set_max_items("max")
+ .set_send_last_published_item("never")
+ .set_access_model("whitelist");
+ return yield pubsub.publish(stream, stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid, NS_URI, conversation_jid.to_string(), node, options);
+ }
+
+ public override void attach(XmppStream stream) {
+ stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NS_URI, on_pubsub_item, null, null);
+ stream.get_module(Presence.Module.IDENTITY).initial_presence_sent.connect(on_initial_presence_sent);
+ if (!stream.has_flag(Flag.IDENTITY)) {
+ stream.add_flag(new Flag());
+ }
+ }
+
+ public override void detach(XmppStream stream) {
+ stream.get_module(Presence.Module.IDENTITY).initial_presence_sent.disconnect(on_initial_presence_sent);
+ stream.get_module(Pubsub.Module.IDENTITY).remove_filtered_notification(stream, NS_URI);
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return IDENTITY.id; }
+
+ }
+
+ public class Flag : XmppStreamFlag {
+ public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "mds");
+
+ private Map<Jid, Jid> stanza_id_bys = new HashMap<Jid, Jid>(Jid.hash_func, Jid.equals_func);
+ private Map<Jid, string> stanza_ids = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
+
+ public void learn_mds(Jid jid, Jid stanza_by, string stanza_id) {
+ stanza_id_bys[jid] = stanza_by;
+ stanza_ids[jid] = stanza_id;
+ }
+
+ public bool is_displayed(Jid conversation_jid, Jid stanza_by, string stanza_id) {
+ return stanza_id_bys[conversation_jid].equals(stanza_by) && stanza_ids[conversation_jid] == stanza_id;
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return IDENTITY.id; }
+ }
+} \ No newline at end of file