aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2025-11-14 14:48:25 -0600
committerfiaxh <git@lightrise.org>2025-11-14 14:48:25 -0600
commit1ede8ade12f26cf39d29d5c616bd28693d677e09 (patch)
treedc0bb08cd95c1b47291e48766dfd3fbd7acbaf40
parent9f02aba799e07ad2ab587334fe59c577e5d4605d (diff)
Add support for message moderation and retraction
-rw-r--r--libdino/meson.build1
-rw-r--r--libdino/src/application.vala1
-rw-r--r--libdino/src/plugin/interfaces.vala1
-rw-r--r--libdino/src/service/message_deletion.vala117
-rw-r--r--libdino/src/service/message_processor.vala2
-rw-r--r--libdino/src/service/muc_manager.vala14
-rw-r--r--main/src/ui/conversation_content_view/conversation_view.vala78
-rw-r--r--main/src/ui/conversation_content_view/file_widget.vala10
-rw-r--r--main/src/ui/conversation_content_view/item_actions.vala61
-rw-r--r--main/src/ui/conversation_content_view/message_widget.vala16
-rw-r--r--main/src/ui/conversation_selector/conversation_selector_row.vala14
-rw-r--r--xmpp-vala/meson.build1
-rw-r--r--xmpp-vala/src/module/xep/0424_message_retraction.vala15
-rw-r--r--xmpp-vala/src/module/xep/0425_message_moderation.vala19
14 files changed, 312 insertions, 38 deletions
diff --git a/libdino/meson.build b/libdino/meson.build
index c5351228..5b537251 100644
--- a/libdino/meson.build
+++ b/libdino/meson.build
@@ -59,6 +59,7 @@ sources = files(
'src/service/history_sync.vala',
'src/service/jingle_file_transfers.vala',
'src/service/message_correction.vala',
+ 'src/service/message_deletion.vala',
'src/service/message_processor.vala',
'src/service/message_storage.vala',
'src/service/module_manager.vala',
diff --git a/libdino/src/application.vala b/libdino/src/application.vala
index de1325ad..737c76c2 100644
--- a/libdino/src/application.vala
+++ b/libdino/src/application.vala
@@ -59,6 +59,7 @@ public interface Application : GLib.Application {
Replies.start(stream_interactor, db);
FallbackBody.start(stream_interactor, db);
ContactModels.start(stream_interactor);
+ MessageDeletion.start(stream_interactor, db);
StatelessFileSharing.start(stream_interactor, db);
create_actions();
diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala
index 86d82bc2..4c5174c6 100644
--- a/libdino/src/plugin/interfaces.vala
+++ b/libdino/src/plugin/interfaces.vala
@@ -164,6 +164,7 @@ public delegate void MessageActionEvoked(Variant? variant);
public class MessageAction : Object {
public string name;
public bool sensitive = true;
+ public bool shortcut_action = true;
public string icon_name;
public string? tooltip;
public Object? popover;
diff --git a/libdino/src/service/message_deletion.vala b/libdino/src/service/message_deletion.vala
new file mode 100644
index 00000000..4467311e
--- /dev/null
+++ b/libdino/src/service/message_deletion.vala
@@ -0,0 +1,117 @@
+using Gee;
+
+using Xmpp;
+using Xmpp.Xep;
+using Dino.Entities;
+using Qlite;
+
+namespace Dino {
+
+ public class MessageDeletion : StreamInteractionModule, MessageListener {
+ public static ModuleIdentity<MessageDeletion> IDENTITY = new ModuleIdentity<MessageDeletion>("message_deletion");
+ public string id { get { return IDENTITY.id; } }
+
+ public signal void item_deleted(ContentItem content_item);
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ MessageDeletion m = new MessageDeletion(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ public MessageDeletion(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+
+ stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(this);
+ }
+
+ public bool is_deletable(Conversation conversation, ContentItem content_item) {
+ MessageItem? message_item = content_item as MessageItem;
+ return message_item != null && message_item.message.body != "";
+ }
+
+ public bool can_delete_for_everyone(Conversation conversation, ContentItem content_item) {
+ if (conversation.type_.is_muc_semantic()) {
+ bool muc_supports_moderation = stream_interactor.get_module(EntityInfo.IDENTITY)
+ .has_feature_cached(conversation.account, conversation.counterpart, Xmpp.Xep.MessageModeration.NS_URI);
+ bool we_are_moderator = stream_interactor.get_module(MucManager.IDENTITY).get_own_role(conversation) == Xmpp.Xep.Muc.Role.MODERATOR;
+ return muc_supports_moderation && we_are_moderator;
+ } else {
+ return content_item.jid.equals_bare(conversation.account.bare_jid);
+ }
+ }
+
+ public void delete_globally(Conversation conversation, ContentItem content_item) {
+ var stream = stream_interactor.get_stream(conversation.account);
+ if (stream == null) return;
+
+ string message_id_to_delete = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
+
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ MessageStanza stanza = new MessageStanza() { to = conversation.counterpart };
+ Xmpp.Xep.MessageRetraction.set_retract_id(stanza, message_id_to_delete);
+ stream.get_module(MessageModule.IDENTITY).send_message(stream, stanza);
+ delete_locally(conversation, content_item, conversation.account.bare_jid);
+ } else if (conversation.type_.is_muc_semantic()) {
+ MessageStanza stanza = new MessageStanza() { to = conversation.counterpart };
+ Xmpp.Xep.MessageModeration.moderate.begin(stream, conversation.counterpart, message_id_to_delete);
+ // Message will be deleted locally when the MUC server sends out a moderation message
+ }
+ }
+
+ public void delete_locally(Conversation conversation, ContentItem content_item, Jid removed_by) {
+ // If it's a file transfer, remove the file
+ if (content_item.type_ == FileItem.TYPE) {
+ FileItem file_item = (FileItem) content_item;
+ if (file_item.file_transfer.path != null) {
+ FileUtils.remove(file_item.file_transfer.path);
+ }
+ }
+
+ // Mark the (underlying) message as removed and clear the body
+ Message? message = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_for_content_item(conversation, content_item);
+ if (message != null) {
+ message.body = "";
+ }
+
+ item_deleted(content_item);
+ }
+
+ public string[] after_actions_const = new string[]{ };
+ public override string action_group { get { return "DELETE"; } }
+ public override string[] after_actions { get { return after_actions_const; } }
+
+ public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ string? delete_message_id = Xep.MessageRetraction.get_retract_id(stanza);
+ if (delete_message_id == null) return false;
+
+ ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_referencing_id(conversation, delete_message_id);
+ if (content_item != null) {
+ debug("Deletion request: %s wants to remove message %s content item id %i. Allowed: %b",
+ message.from.to_string(), delete_message_id, content_item.id,
+ is_removal_allowed(conversation, content_item, stanza.from));
+ delete_locally(conversation, content_item, stanza.from);
+ }
+
+ return false;
+ }
+
+ private bool is_removal_allowed(Conversation conversation, ContentItem content_item, Jid removed_by) {
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ return removed_by.equals_bare(content_item.jid);
+ } else if (conversation.type_.is_muc_semantic()) {
+ // Only accept MUC message removals if the MUC server announced support.
+ // MUC moderations should always come from the MUC bare JID.
+ bool muc_supports_moderation = stream_interactor.get_module(EntityInfo.IDENTITY)
+ .has_feature_cached(conversation.account, conversation.counterpart, Xmpp.Xep.MessageModeration.NS_URI);
+ return muc_supports_moderation && removed_by.equals(conversation.counterpart);
+ }
+
+ return false;
+ }
+ }
+
+}
diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala
index b9e64923..51fe1808 100644
--- a/libdino/src/service/message_processor.vala
+++ b/libdino/src/service/message_processor.vala
@@ -311,7 +311,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
private class FilterMessageListener : MessageListener {
- public string[] after_actions_const = new string[]{ "DECRYPT" };
+ public string[] after_actions_const = new string[]{ "DECRYPT", "DELETE" };
public override string action_group { get { return "FILTER_EMPTY"; } }
public override string[] after_actions { get { return after_actions_const; } }
diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala
index 5b251b07..fd8b4336 100644
--- a/libdino/src/service/muc_manager.vala
+++ b/libdino/src/service/muc_manager.vala
@@ -353,6 +353,13 @@ public class MucManager : StreamInteractionModule, Object {
return null;
}
+ public Xep.Muc.Role? get_own_role(Conversation conversation) {
+ Xmpp.Jid? own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account);
+ if (own_jid == null) return null;
+
+ return stream_interactor.get_module(MucManager.IDENTITY).get_role(own_jid, conversation.account);
+ }
+
public Xep.Muc.Affiliation? get_affiliation(Jid muc_jid, Jid jid, Account account) {
Xep.Muc.Flag? flag = get_muc_flag(account);
if (flag != null) {
@@ -698,6 +705,13 @@ public class MucManager : StreamInteractionModule, Object {
message.real_jid = real_jid.bare_jid;
}
}
+
+ string? occupant_id = Xep.OccupantIds.get_occupant_id(stanza.stanza);
+ if (occupant_id != null) {
+ message.occupant_db_id = stream_interactor.get_module(OccupantIdStore.IDENTITY)
+ .cache_occupant_id(conversation.account, occupant_id, message.from);
+ }
+
Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(message.counterpart.bare_jid, conversation.account);
if (stanza.id != null && own_muc_jid != null && message.from.equals(own_muc_jid)) {
Entities.Message? m = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza.id, conversation);
diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala
index 257a659b..233d3312 100644
--- a/main/src/ui/conversation_content_view/conversation_view.vala
+++ b/main/src/ui/conversation_content_view/conversation_view.vala
@@ -51,26 +51,45 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
// Setup all message menu buttons
var correction_button = new Button() { name="correction" };
correction_button.clicked.connect((button) => {
- on_action_button_clicked(button, null);
+ on_action_button_clicked("correction", null);
});
action_buttons["correction"] = correction_button;
message_menu_box.append(correction_button);
- var reply_button = new Button() { name="reply" };
- reply_button.clicked.connect((button) => {
- on_action_button_clicked(button, null);
- });
- action_buttons["reply"] = reply_button;
- message_menu_box.append(reply_button);
-
var reaction_button = new MenuButton() { name="reaction" };
EmojiChooser chooser = new EmojiChooser();
chooser.emoji_picked.connect((emoji) => {
- on_action_button_clicked(reaction_button, new GLib.Variant.string(emoji));
+ on_action_button_clicked("reaction", new GLib.Variant.string(emoji));
});
reaction_button.popover = chooser;
action_buttons["reaction"] = reaction_button;
message_menu_box.append(reaction_button);
+
+ var reply_button = new Button() { name="reply" };
+ reply_button.clicked.connect((button) => {
+ on_action_button_clicked("reply", null);
+ });
+ action_buttons["reply"] = reply_button;
+ message_menu_box.append(reply_button);
+
+ var delete_button = new Button() { name="delete" };
+ delete_button.clicked.connect((button) => {
+ on_action_button_clicked("delete", null);
+ });
+ action_buttons["delete"] = delete_button;
+ message_menu_box.append(delete_button);
+
+ var menu_button = new MenuButton() { name="menu", tooltip_text=_("More actions") };
+ action_buttons["menu"] = menu_button;
+ message_menu_box.append(menu_button);
+
+ SimpleAction action_action = new SimpleAction("action", VariantType.STRING);
+ action_action.activate.connect((parameter) => {
+ on_action_button_clicked(parameter.get_string());
+ });
+ SimpleActionGroup action_group = new SimpleActionGroup();
+ action_group.insert(action_action);
+ this.insert_action_group("action", action_group);
}
public ConversationView init(StreamInteractor stream_interactor) {
@@ -139,7 +158,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
private bool is_highlight_fixed() {
foreach (Widget widget in action_buttons.values) {
MenuButton? menu_button = widget as MenuButton;
- if (menu_button != null && menu_button.popover.visible) return true;
+ if (menu_button != null && menu_button.popover != null && menu_button.popover.visible) return true;
ToggleButton? toggle_button = widget as ToggleButton;
if (toggle_button != null && toggle_button.active) return true;
@@ -224,24 +243,35 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
if (message_actions != null) {
message_menu_box.visible = true;
+ Menu menu_model = new Menu();
+ ((MenuButton)action_buttons["menu"]).menu_model = menu_model;
+
foreach (Widget widget in action_buttons.values) {
widget.visible = false;
}
// Configure as many buttons as we need with the actions for the current meta item
foreach (var message_action in current_message_actions) {
- Widget button_widget = action_buttons[message_action.name];
- button_widget.visible = true;
- if (message_action.name == "reaction") {
- MenuButton button = (MenuButton) button_widget;
- button.sensitive = message_action.sensitive;
- button.icon_name = message_action.icon_name;
- button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
- } else if (message_action.callback != null) {
- Button button = (Button) button_widget;
- button.sensitive = message_action.sensitive;
- button.icon_name = message_action.icon_name;
- button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
+ if (message_action.shortcut_action) {
+ Widget button_widget = action_buttons[message_action.name];
+ button_widget.visible = true;
+
+ if (message_action.name == "reaction") {
+ MenuButton button = (MenuButton) button_widget;
+ button.sensitive = message_action.sensitive;
+ button.icon_name = message_action.icon_name;
+ button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
+ } else if (message_action.callback != null) {
+ Button button = (Button) button_widget;
+ button.sensitive = message_action.sensitive;
+ button.icon_name = message_action.icon_name;
+ button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
+ }
+ } else {
+ MenuItem item = new MenuItem(message_action.tooltip, null);
+ item.set_action_and_target_value("action.action", new GLib.Variant.string(message_action.name));
+ menu_model.append_item(item);
+ action_buttons["menu"].visible = true;
}
}
} else {
@@ -510,9 +540,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
(upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND);
}
- private void on_action_button_clicked(Widget widget, GLib.Variant? variant = null) {
+ private void on_action_button_clicked(string action_name, GLib.Variant? variant = null) {
foreach (var action in message_actions) {
- if (action.name != widget.name) continue;
+ if (action.name != action_name) continue;
action.callback(variant);
}
}
diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala
index 0e3cee03..cb734e5f 100644
--- a/main/src/ui/conversation_content_view/file_widget.vala
+++ b/main/src/ui/conversation_content_view/file_widget.vala
@@ -31,10 +31,12 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
- if (stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(file_item.conversation, content_item) != null) {
- actions.add(get_reply_action(content_item, file_item.conversation, stream_interactor));
- actions.add(get_reaction_action(content_item, file_item.conversation, stream_interactor));
- }
+ actions.add(get_reply_action(content_item, file_item.conversation, stream_interactor));
+ actions.add(get_reaction_action(content_item, file_item.conversation, stream_interactor));
+
+ var delete_action = get_delete_action(content_item, file_item.conversation, stream_interactor);
+ if (delete_action != null) actions.add(delete_action);
+
return actions;
}
}
diff --git a/main/src/ui/conversation_content_view/item_actions.vala b/main/src/ui/conversation_content_view/item_actions.vala
index d4195bdd..6102dc9a 100644
--- a/main/src/ui/conversation_content_view/item_actions.vala
+++ b/main/src/ui/conversation_content_view/item_actions.vala
@@ -48,4 +48,65 @@ namespace Dino.Ui {
}
return action;
}
+
+ public Plugins.MessageAction? get_delete_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) {
+ bool is_deletable = stream_interactor.get_module(MessageDeletion.IDENTITY).is_deletable(conversation, content_item);
+ if (!is_deletable) return null;
+
+ bool can_delete_for_everyone = stream_interactor.get_module(MessageDeletion.IDENTITY).can_delete_for_everyone(conversation, content_item);
+
+ Plugins.MessageAction action = new Plugins.MessageAction();
+ action.name = "delete";
+ action.icon_name = "user-trash-symbolic";
+ action.tooltip = _("Delete message");
+ action.shortcut_action = false;
+ action.callback = () => {
+ Adw.AlertDialog dialog = new Adw.AlertDialog(_("Delete message"), null) { content_width = 350, follows_content_size = false };
+ if (can_delete_for_everyone) {
+ CheckButton check_button = new CheckButton() { label="Delete for everyone", active=true };
+
+ Box extra_child = new Box(Orientation.VERTICAL, 8);
+ extra_child.append(new Label("Would you like to remove this message for everyone or just for yourself?") { xalign=0, use_markup=true, wrap=true });
+ extra_child.append(new Label("The message will only be deleted for others <b>if their app supports it</b>!") { xalign=0, use_markup=true, wrap=true });
+ extra_child.append(check_button);
+ dialog.extra_child = extra_child;
+
+ check_button.toggled.connect(() => {
+ dialog.remove_response("delete");
+ dialog.remove_response("cancel");
+ if (check_button.active) {
+ dialog.add_response("delete", "Delete for everyone");
+ } else {
+ dialog.add_response("delete", "Only delete for yourself");
+ }
+ dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE);
+ dialog.add_response("cancel", "Cancel");
+ });
+ dialog.response.connect((response) => {
+ if (response == "delete") {
+ if (check_button.active) {
+ stream_interactor.get_module(MessageDeletion.IDENTITY).delete_globally(conversation, content_item);
+ } else {
+ stream_interactor.get_module(MessageDeletion.IDENTITY).delete_locally(conversation, content_item, conversation.account.bare_jid);
+ }
+ }
+ });
+ dialog.add_response("delete", "Delete for everyone");
+ dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE);
+ dialog.add_response("cancel", "Cancel");
+ } else {
+ dialog.extra_child = new Label("This message <b>can't be deleted</b> for everyone. Would you like to remove it only for yourself?") { use_markup=true, wrap=true };
+ dialog.add_response("delete", "Only delete for myself");
+ dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE);
+ dialog.add_response("cancel", "Cancel");
+ dialog.response.connect((response) => {
+ if (response == "delete") {
+ stream_interactor.get_module(MessageDeletion.IDENTITY).delete_locally(conversation, content_item, conversation.account.bare_jid);
+ }
+ });
+ }
+ dialog.present(((Dino.Ui.Application)Dino.Application.get_default()).window);
+ };
+ return action;
+ }
} \ No newline at end of file
diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala
index d116e7de..5f0c067a 100644
--- a/main/src/ui/conversation_content_view/message_widget.vala
+++ b/main/src/ui/conversation_content_view/message_widget.vala
@@ -36,7 +36,8 @@ public class MessageMetaItem : ContentMetaItem {
message_item = content_item as MessageItem;
this.stream_interactor = stream_interactor;
- stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction);
+ stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_updated_item);
+ stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect(on_updated_item);
label.activate_link.connect(on_label_activate_link);
@@ -157,6 +158,10 @@ public class MessageMetaItem : ContentMetaItem {
string dim_color = Util.is_dark_theme(this.label) ? "#BDBDBD" : "#707070";
+ if (message.body == "") {
+ markup_text = @"<i><span size='small' color='$dim_color'>%s</span></i>".printf(_("Message deleted"));
+ theme_dependent = true;
+ }
if (message.edit_to != null) {
markup_text += @" <span size='small' color='$dim_color'>(%s)</span>".printf(_("edited"));
theme_dependent = true;
@@ -231,6 +236,7 @@ public class MessageMetaItem : ContentMetaItem {
action1.name = "correction";
action1.icon_name = "document-edit-symbolic";
action1.tooltip = _("Edit message");
+ action1.shortcut_action = false;
action1.callback = () => {
this.in_edit_mode = true;
};
@@ -240,6 +246,9 @@ public class MessageMetaItem : ContentMetaItem {
actions.add(get_reply_action(content_item, message_item.conversation, stream_interactor));
actions.add(get_reaction_action(content_item, message_item.conversation, stream_interactor));
+ var delete_action = get_delete_action(content_item, message_item.conversation, stream_interactor);
+ if (delete_action != null) actions.add(delete_action);
+
return actions;
}
@@ -277,7 +286,7 @@ public class MessageMetaItem : ContentMetaItem {
}
}
- private void on_received_correction(ContentItem content_item) {
+ private void on_updated_item(ContentItem content_item) {
if (this.content_item.id == content_item.id) {
this.content_item = content_item;
message_item = content_item as MessageItem;
@@ -294,7 +303,8 @@ public class MessageMetaItem : ContentMetaItem {
}
public override void dispose() {
- stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_received_correction);
+ stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_updated_item);
+ stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.disconnect(on_updated_item);
this.notify["in-edit-mode"].disconnect(on_in_edit_mode_changed);
if (marked_notify_handler_id != -1) {
this.disconnect(marked_notify_handler_id);
diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala
index a0e8b11b..7410937a 100644
--- a/main/src/ui/conversation_selector/conversation_selector_row.vala
+++ b/main/src/ui/conversation_selector/conversation_selector_row.vala
@@ -77,6 +77,11 @@ public class ConversationSelectorRow : ListBoxRow {
content_item_received(item);
}
});
+ stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect((item) => {
+ if (last_content_item != null && last_content_item.id == item.id) {
+ content_item_received(item);
+ }
+ });
last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation);
@@ -161,8 +166,13 @@ public class ConversationSelectorRow : ListBoxRow {
nick_label.label += ": ";
}
- change_label_attribute(message_label, attr_style_new(Pango.Style.NORMAL));
- message_label.label = Util.summarize_whitespaces_to_space(body);
+ if (message_item.message.body == "") {
+ change_label_attribute(message_label, attr_style_new(Pango.Style.ITALIC));
+ message_label.label = _("Message deleted");
+ } else {
+ change_label_attribute(message_label, attr_style_new(Pango.Style.NORMAL));
+ message_label.label = Util.summarize_whitespaces_to_space(body);
+ }
break;
case FileItem.TYPE:
diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build
index 4aa6e328..d4bfc9c2 100644
--- a/xmpp-vala/meson.build
+++ b/xmpp-vala/meson.build
@@ -125,6 +125,7 @@ sources = files(
'src/module/xep/0410_muc_self_ping.vala',
'src/module/xep/0421_occupant_ids.vala',
'src/module/xep/0424_message_retraction.vala',
+ 'src/module/xep/0425_message_moderation.vala',
'src/module/xep/0428_fallback_indication.vala',
'src/module/xep/0444_reactions.vala',
'src/module/xep/0446_file_metadata_element.vala',
diff --git a/xmpp-vala/src/module/xep/0424_message_retraction.vala b/xmpp-vala/src/module/xep/0424_message_retraction.vala
index 747f4c08..3e60eb2b 100644
--- a/xmpp-vala/src/module/xep/0424_message_retraction.vala
+++ b/xmpp-vala/src/module/xep/0424_message_retraction.vala
@@ -1,12 +1,19 @@
namespace Xmpp.Xep.MessageRetraction {
- private const string NS_URI = "urn:xmpp:message-retract:1";
+ public const string NS_URI = "urn:xmpp:message-retract:1";
public static string? get_retract_id(MessageStanza message) {
- StanzaNode? node = message.stanza.get_subnode("retract", NS_URI);
- if (node == null) return null;
+ StanzaNode? retract_node = message.stanza.get_subnode("retract", NS_URI);
+ if (retract_node == null) return null;
- return node.get_attribute("id");
+ return retract_node.get_attribute("id");
+ }
+
+ public static void set_retract_id(MessageStanza message, string message_id) {
+ StanzaNode retract_node = (new StanzaNode.build("retract", NS_URI))
+ .add_self_xmlns()
+ .put_attribute("id", message_id);
+ message.stanza.put_node(retract_node);
}
public class Module : XmppStreamModule {
diff --git a/xmpp-vala/src/module/xep/0425_message_moderation.vala b/xmpp-vala/src/module/xep/0425_message_moderation.vala
new file mode 100644
index 00000000..80fab905
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0425_message_moderation.vala
@@ -0,0 +1,19 @@
+namespace Xmpp.Xep.MessageModeration {
+
+ public const string NS_URI = "urn:xmpp:message-moderate:1";
+
+
+ /**
+ * @return whether the moderation was successful
+ */
+ public static async bool moderate(XmppStream stream, Jid muc_jid, string message_id) {
+ StanzaNode moderate_node = new StanzaNode.build("moderate", NS_URI)
+ .add_self_xmlns()
+ .put_attribute("id", message_id)
+ .put_node(new StanzaNode.build("retract", Xep.MessageRetraction.NS_URI).add_self_xmlns());
+ Iq.Stanza iq = new Iq.Stanza.set(moderate_node) { to = muc_jid };
+
+ Iq.Stanza result_stanza = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq);
+ return !result_stanza.is_error();
+ }
+} \ No newline at end of file