Support replies and reactions to files

This commit is contained in:
fiaxh 2023-01-06 16:14:47 +01:00
parent 0c4aea96ff
commit cb3b19b01d
12 changed files with 202 additions and 102 deletions

View File

@ -44,11 +44,8 @@ public class ContentItemStore : StreamInteractionModule, Object {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func);
foreach (var row in select) {
int id = row[db.content_item.id];
int content_type = row[db.content_item.content_type];
int foreign_id = row[db.content_item.foreign_id];
DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]);
items.add(get_item(conversation, id, content_type, foreign_id, time));
ContentItem content_item = get_item_from_row(row, conversation);
items.add(content_item);
}
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
@ -58,6 +55,14 @@ public class ContentItemStore : StreamInteractionModule, Object {
return ret;
}
private ContentItem get_item_from_row(Row row, Conversation conversation) throws Error {
int id = row[db.content_item.id];
int content_type = row[db.content_item.content_type];
int foreign_id = row[db.content_item.foreign_id];
DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]);
return get_item(conversation, id, content_type, foreign_id, time);
}
private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
switch (content_type) {
case 1:
@ -112,6 +117,86 @@ public class ContentItemStore : StreamInteractionModule, Object {
return item.size > 0 ? item[0] : null;
}
public string? get_message_id_for_content_item(Conversation conversation, ContentItem content_item) {
Message? message = get_message_for_content_item(conversation, content_item);
if (message == null) return null;
if (conversation.type_ == Conversation.Type.CHAT) {
return message.stanza_id;
} else {
return message.server_id;
}
}
public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) {
Message? message = get_message_for_content_item(conversation, content_item);
if (message == null) return null;
return message.from;
}
private Message? get_message_for_content_item(Conversation conversation, ContentItem content_item) {
FileItem? file_item = content_item as FileItem;
if (file_item != null) {
if (file_item.file_transfer.provider != 0 || file_item.file_transfer.info == null) return null;
int message_db_id = int.parse(file_item.file_transfer.info);
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_db_id, conversation);
}
MessageItem? message_item = content_item as MessageItem;
if (message_item != null) {
return message_item.message;
}
return null;
}
public ContentItem? get_content_item_for_message_id(Conversation conversation, string message_id) {
Row? row = get_content_item_row_for_message_id(conversation, message_id);
if (row != null) {
return get_item_from_row(row, conversation);
}
return null;
}
public int get_content_item_id_for_message_id(Conversation conversation, string message_id) {
Row? row = get_content_item_row_for_message_id(conversation, message_id);
if (row != null) {
return row[db.content_item.id];
}
return -1;
}
private Row? get_content_item_row_for_message_id(Conversation conversation, string message_id) {
var content_item_row = db.content_item.select();
Message? message = null;
if (conversation.type_ == Conversation.Type.CHAT) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation);
} else {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation);
}
if (message == null) return null;
RowOption file_transfer_row = db.file_transfer.select()
.with(db.file_transfer.account_id, "=", conversation.account.id)
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(conversation.counterpart))
.with(db.file_transfer.info, "=", message.id.to_string())
.order_by(db.file_transfer.time, "DESC")
.single().row();
if (file_transfer_row.is_present()) {
content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id])
.with(db.content_item.content_type, "=", 2);
} else {
content_item_row.with(db.content_item.foreign_id, "=", message.id)
.with(db.content_item.content_type, "=", 1);
}
RowOption content_item_row_option = content_item_row.single().row();
if (content_item_row_option.is_present()) {
return content_item_row_option.inner;
}
return null;
}
public ContentItem? get_latest(Conversation conversation) {
Gee.List<ContentItem> items = get_n_latest(conversation, 1);
if (items.size > 0) {

View File

@ -425,24 +425,8 @@ public class MessageProcessor : StreamInteractionModule, Object {
new_message.type_ = MessageStanza.TYPE_CHAT;
}
if (message.quoted_item_id > 0) {
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id);
if (content_item != null && content_item.type_ == MessageItem.TYPE) {
Message? quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(((MessageItem) content_item).message.id, conversation);
if (quoted_message != null) {
Xep.Replies.set_reply_to(new_message, new Xep.Replies.ReplyTo(quoted_message.from, quoted_message.stanza_id));
string body_with_fallback = "> " + Dino.message_body_without_reply_fallback(quoted_message);
body_with_fallback = body_with_fallback.replace("\n", "\n> ");
body_with_fallback += "\n";
long fallback_length = body_with_fallback.length;
body_with_fallback += message.body;
new_message.body = body_with_fallback;
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
Xep.FallbackIndication.set_fallback(new_message, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location }));
}
}
}
string? fallback = get_fallback_body_set_infos(message, new_message, conversation);
new_message.body = fallback == null ? message.body : fallback + message.body;
build_message_stanza(message, new_message, conversation);
pre_message_send(message, new_message, conversation);
@ -487,6 +471,37 @@ public class MessageProcessor : StreamInteractionModule, Object {
}
});
}
public string? get_fallback_body_set_infos(Entities.Message message, MessageStanza new_stanza, Conversation conversation) {
if (message.quoted_item_id == 0) return null;
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id);
if (content_item == null) return null;
Jid? quoted_sender = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_sender_for_content_item(conversation, content_item);
string? quoted_stanza_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
if (quoted_sender != null && quoted_stanza_id != null) {
Xep.Replies.set_reply_to(new_stanza, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id));
}
string fallback = "> ";
if (content_item.type_ == MessageItem.TYPE) {
Message? quoted_message = ((MessageItem) content_item).message;
fallback += Dino.message_body_without_reply_fallback(quoted_message);
fallback = fallback.replace("\n", "\n> ");
} else if (content_item.type_ == FileItem.TYPE) {
FileTransfer? quoted_file = ((FileItem) content_item).file_transfer;
fallback += quoted_file.file_name;
}
fallback += "\n";
long fallback_length = fallback.length;
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
Xep.FallbackIndication.set_fallback(new_stanza, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location }));
return fallback;
}
}
public abstract class MessageListener : Xmpp.OrderedListener {

View File

@ -116,9 +116,7 @@ public class MessageStorage : StreamInteractionModule, Object {
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.outer_join_with(db.reply, db.reply.message_id, db.message.id);
if (conversation.counterpart.resourcepart == null) {
query.with_null(db.message.counterpart_resource);
} else {
if (conversation.counterpart.resourcepart != null) {
query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart);
}

View File

@ -10,7 +10,6 @@ public class Dino.Reactions : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } }
public signal void reaction_added(Account account, int content_item_id, Jid jid, string reaction);
// [Signal(detailed=true)]
public signal void reaction_removed(Account account, int content_item_id, Jid jid, string reaction);
private StreamInteractor stream_interactor;
@ -35,15 +34,19 @@ public class Dino.Reactions : StreamInteractionModule, Object {
if (!reactions.contains(reaction)) {
reactions.add(reaction);
}
send_reactions(conversation, content_item, reactions);
reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
try {
send_reactions(conversation, content_item, reactions);
reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
} catch (SendError e) {}
}
public void remove_reaction(Conversation conversation, ContentItem content_item, string reaction) {
Gee.List<string> reactions = get_own_reactions(conversation, content_item);
reactions.remove(reaction);
send_reactions(conversation, content_item, reactions);
reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
try {
send_reactions(conversation, content_item, reactions);
reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
} catch (SendError e) {}
}
public Gee.List<ReactionUsers> get_item_reactions(Conversation conversation, ContentItem content_item) {
@ -80,35 +83,28 @@ public class Dino.Reactions : StreamInteractionModule, Object {
return false;
}
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) {
Message? message = null;
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) throws SendError {
string? message_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
if (message_id == null) throw new SendError.Misc("No message for content_item");
FileItem? file_item = content_item as FileItem;
if (file_item != null) {
int message_id = int.parse(file_item.file_transfer.info);
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_id, conversation);
}
MessageItem? message_item = content_item as MessageItem;
if (message_item != null) {
message = message_item.message;
}
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream == null) throw new SendError.NoStream("");
if (message == null) {
return;
}
var reactions_module = stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY);
XmppStream stream = stream_interactor.get_stream(conversation.account);
if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "groupchat", message.server_id ?? message.stanza_id, reactions);
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.server_id ?? message.stanza_id, reactions);
}
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
reactions_module.send_reaction.begin(stream, conversation.counterpart, "groupchat", message_id, reactions);
// We save the reaction when it gets reflected back to us
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
reactions_module.send_reaction(stream, conversation.counterpart, "chat", message_id, reactions);
} else if (conversation.type_ == Conversation.Type.CHAT) {
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.stanza_id, reactions);
int64 now_millis = GLib.get_real_time () / 1000;
save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_millis, reactions);
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions, (_, res) => {
try {
reactions_module.send_reaction.end(res);
save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_millis, reactions);
} catch (SendError e) {}
});
}
}
@ -251,11 +247,11 @@ public class Dino.Reactions : StreamInteractionModule, Object {
Message reaction_message = yield stream_interactor.get_module(MessageProcessor.IDENTITY).parse_message_stanza(account, stanza);
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(reaction_message);
Message? message = get_message_for_reaction(conversation, message_id);
int content_item_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_id_for_message_id(conversation, message_id);
var reaction_info = new ReactionInfo() { account=account, from_jid=from_jid, reactions=reactions, stanza=stanza, received_time=new DateTime.now() };
if (message != null) {
process_reaction_for_message(message.id, reaction_info);
if (content_item_id != -1) {
process_reaction_for_message(content_item_id, reaction_info);
return;
}
@ -317,30 +313,12 @@ public class Dino.Reactions : StreamInteractionModule, Object {
}
}
private void process_reaction_for_message(int message_db_id, ReactionInfo reaction_info) {
private void process_reaction_for_message(int content_item_id, ReactionInfo reaction_info) {
Account account = reaction_info.account;
MessageStanza stanza = reaction_info.stanza;
Jid from_jid = reaction_info.from_jid;
Gee.List<string> reactions = reaction_info.reactions;
RowOption file_transfer_row = db.file_transfer.select()
.with(db.file_transfer.account_id, "=", account.id)
.with(db.file_transfer.info, "=", message_db_id.to_string())
.single().row(); // TODO better
var content_item_row = db.content_item.select();
if (file_transfer_row.is_present()) {
content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id])
.with(db.content_item.content_type, "=", 2);
} else {
content_item_row.with(db.content_item.foreign_id, "=", message_db_id)
.with(db.content_item.content_type, "=", 1);
}
var content_item_row_opt = content_item_row.single().row();
if (!content_item_row_opt.is_present()) return;
int content_item_id = content_item_row_opt[db.content_item.id];
// Get reaction time
DateTime? reaction_time = null;
DelayedDelivery.MessageFlag? delayed_message_flag = DelayedDelivery.MessageFlag.get_flag(stanza);

View File

@ -77,22 +77,7 @@ public class Dino.Replies : StreamInteractionModule, Object {
Xep.Replies.ReplyTo? reply_to = Xep.Replies.get_reply_to(stanza);
if (reply_to == null) return;
Message? quoted_message = null;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(reply_to.to_message_id, conversation);
} else {
quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(reply_to.to_message_id, conversation);
}
if (quoted_message == null) {
db.reply.upsert()
.value(db.reply.message_id, message.id, true)
.value(db.reply.quoted_message_stanza_id, reply_to.to_message_id)
.value(db.reply.quoted_message_from, reply_to.to_jid.to_string())
.perform();
return;
}
ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, quoted_message.id);
ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_message_id(conversation, reply_to.to_message_id);
if (quoted_content_item == null) return;
set_message_is_reply_to(message, quoted_content_item);

View File

@ -1,4 +1,5 @@
using Dino.Entities;
using Qlite;
namespace Dino {

View File

@ -10,19 +10,44 @@ namespace Dino.Ui {
public class FileMetaItem : ConversationSummary.ContentMetaItem {
private StreamInteractor stream_interactor;
private FileItem file_item;
private FileTransfer file_transfer;
public FileMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
base(content_item);
this.stream_interactor = stream_interactor;
this.file_item = content_item as FileItem;
this.file_transfer = file_item.file_transfer;
}
public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) {
FileItem file_item = content_item as FileItem;
FileTransfer transfer = file_item.file_transfer;
return new FileWidget(stream_interactor, transfer);
return new FileWidget(stream_interactor, file_transfer);
}
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
if (file_transfer.provider != 0 || file_transfer.info == null) return null;
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) {
Plugins.MessageAction reply_action = new Plugins.MessageAction();
reply_action.icon_name = "mail-reply-sender-symbolic";
reply_action.callback = (button, content_meta_item_activated, widget) => {
GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(file_item.conversation.id), new GLib.Variant.int32(content_item.id) }));
};
actions.add(reply_action);
Plugins.MessageAction action2 = new Plugins.MessageAction();
action2.icon_name = "dino-emoticon-add-symbolic";
EmojiChooser chooser = new EmojiChooser();
chooser.emoji_picked.connect((emoji) => {
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(file_item.conversation, content_item, emoji);
});
action2.popover = chooser;
actions.add(action2);
}
return actions;
}
}
public class FileWidget : SizeRequestBox {

View File

@ -198,7 +198,7 @@ public class MessageMetaItem : ContentMetaItem {
if (quoted_content_item != null) {
var quote_model = new Quote.Model.from_content_item(quoted_content_item, message_item.conversation, stream_interactor);
quote_model.jump_to.connect(() => {
GLib.Application.get_default().activate_action("jump-to-conversation-message", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) }));
GLib.Application.get_default().activate_action("jump-to-conversation-message", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(quoted_content_item.id) }));
});
var quote_widget = Quote.get_widget(quote_model);
outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1);
@ -226,7 +226,7 @@ public class MessageMetaItem : ContentMetaItem {
Plugins.MessageAction reply_action = new Plugins.MessageAction();
reply_action.icon_name = "mail-reply-sender-symbolic";
reply_action.callback = (button, content_meta_item_activated, widget) => {
GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) }));
GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) }));
};
actions.add(reply_action);

View File

@ -27,7 +27,8 @@ namespace Dino.Ui.Quote {
var message = ((MessageItem) content_item).message;
this.message = Dino.message_body_without_reply_fallback(message);
} else if (content_item.type_ == FileItem.TYPE) {
this.message = "[File]";
var file_transfer = ((FileItem) content_item).file_transfer;
this.message = _("File") + ": " + file_transfer.file_name;
}
this.message_time = content_item.time;

View File

@ -17,7 +17,11 @@ namespace Xmpp {
public async void send_message(XmppStream stream, MessageStanza message) throws IOStreamError {
yield send_pipeline.run(stream, message);
yield stream.write_async(message.stanza);
try {
yield stream.write_async(message.stanza);
} catch (IOStreamError e) {
throw new SendError.IO(e.message);
}
}
public async void received_message_stanza_async(XmppStream stream, StanzaNode node) {

View File

@ -11,7 +11,7 @@ public class Module : XmppStreamModule {
private ReceivedPipelineListener received_pipeline_listener = new ReceivedPipelineListener();
public void send_reaction(XmppStream stream, Jid jid, string stanza_type, string message_id, Gee.List<string> reactions) {
public async void send_reaction(XmppStream stream, Jid jid, string stanza_type, string message_id, Gee.List<string> reactions) throws SendError {
StanzaNode reactions_node = new StanzaNode.build("reactions", NS_URI).add_self_xmlns();
reactions_node.put_attribute("id", message_id);
foreach (string reaction in reactions) {
@ -25,7 +25,7 @@ public class Module : XmppStreamModule {
MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_STORE);
stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
yield stream.get_module(MessageModule.IDENTITY).send_message(stream, message);
}
public override void attach(XmppStream stream) {

View File

@ -38,3 +38,11 @@ public long from_hex(string numeral) {
}
}
namespace Xmpp {
public errordomain SendError {
IO,
NoStream,
Misc
}
}