Add support for XEP-0461 replies (with fallback)
This commit is contained in:
parent
4d7809bb12
commit
dc52e7595c
|
@ -40,6 +40,7 @@ SOURCES
|
||||||
src/service/database.vala
|
src/service/database.vala
|
||||||
src/service/entity_capabilities_storage.vala
|
src/service/entity_capabilities_storage.vala
|
||||||
src/service/entity_info.vala
|
src/service/entity_info.vala
|
||||||
|
src/service/fallback_body.vala
|
||||||
src/service/file_manager.vala
|
src/service/file_manager.vala
|
||||||
src/service/file_transfer_storage.vala
|
src/service/file_transfer_storage.vala
|
||||||
src/service/history_sync.vala
|
src/service/history_sync.vala
|
||||||
|
@ -51,6 +52,7 @@ SOURCES
|
||||||
src/service/muc_manager.vala
|
src/service/muc_manager.vala
|
||||||
src/service/notification_events.vala
|
src/service/notification_events.vala
|
||||||
src/service/presence_manager.vala
|
src/service/presence_manager.vala
|
||||||
|
src/service/replies.vala
|
||||||
src/service/reactions.vala
|
src/service/reactions.vala
|
||||||
src/service/registration.vala
|
src/service/registration.vala
|
||||||
src/service/roster_manager.vala
|
src/service/roster_manager.vala
|
||||||
|
|
|
@ -56,6 +56,8 @@ public interface Application : GLib.Application {
|
||||||
MessageCorrection.start(stream_interactor, db);
|
MessageCorrection.start(stream_interactor, db);
|
||||||
FileTransferStorage.start(stream_interactor, db);
|
FileTransferStorage.start(stream_interactor, db);
|
||||||
Reactions.start(stream_interactor, db);
|
Reactions.start(stream_interactor, db);
|
||||||
|
Replies.start(stream_interactor, db);
|
||||||
|
FallbackBody.start(stream_interactor, db);
|
||||||
|
|
||||||
create_actions();
|
create_actions();
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,9 @@ public class Message : Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string? edit_to = null;
|
public string? edit_to = null;
|
||||||
|
public int quoted_item_id = 0;
|
||||||
|
|
||||||
|
private Gee.List<Xep.FallbackIndication.Fallback> fallbacks = null;
|
||||||
|
|
||||||
private Database? db;
|
private Database? db;
|
||||||
|
|
||||||
|
@ -105,6 +108,7 @@ public class Message : Object {
|
||||||
if (real_jid_str != null) real_jid = new Jid(real_jid_str);
|
if (real_jid_str != null) real_jid = new Jid(real_jid_str);
|
||||||
|
|
||||||
edit_to = row[db.message_correction.to_stanza_id];
|
edit_to = row[db.message_correction.to_stanza_id];
|
||||||
|
quoted_item_id = row[db.reply.quoted_content_item_id];
|
||||||
|
|
||||||
notify.connect(on_update);
|
notify.connect(on_update);
|
||||||
}
|
}
|
||||||
|
@ -138,6 +142,32 @@ public class Message : Object {
|
||||||
notify.connect(on_update);
|
notify.connect(on_update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Gee.List<Xep.FallbackIndication.Fallback> get_fallbacks() {
|
||||||
|
if (fallbacks != null) return fallbacks;
|
||||||
|
|
||||||
|
var fallbacks_by_ns = new HashMap<string, ArrayList<Xep.FallbackIndication.FallbackLocation>>();
|
||||||
|
foreach (Qlite.Row row in db.body_meta.select().with(db.body_meta.message_id, "=", id)) {
|
||||||
|
if (row[db.body_meta.info_type] != Xep.FallbackIndication.NS_URI) continue;
|
||||||
|
|
||||||
|
string ns_uri = row[db.body_meta.info];
|
||||||
|
if (!fallbacks_by_ns.has_key(ns_uri)) {
|
||||||
|
fallbacks_by_ns[ns_uri] = new ArrayList<Xep.FallbackIndication.FallbackLocation>();
|
||||||
|
}
|
||||||
|
fallbacks_by_ns[ns_uri].add(new Xep.FallbackIndication.FallbackLocation(row[db.body_meta.from_char], row[db.body_meta.to_char]));
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallbacks = new ArrayList<Xep.FallbackIndication.Fallback>();
|
||||||
|
foreach (string ns_uri in fallbacks_by_ns.keys) {
|
||||||
|
fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri].to_array()));
|
||||||
|
}
|
||||||
|
this.fallbacks = fallbacks;
|
||||||
|
return fallbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_fallbacks(Gee.List<Xep.FallbackIndication.Fallback> fallbacks) {
|
||||||
|
this.fallbacks = fallbacks;
|
||||||
|
}
|
||||||
|
|
||||||
public void set_type_string(string type) {
|
public void set_type_string(string type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Xmpp.MessageStanza.TYPE_CHAT:
|
case Xmpp.MessageStanza.TYPE_CHAT:
|
||||||
|
@ -210,6 +240,13 @@ public class Message : Object {
|
||||||
.value(db.real_jid.real_jid, real_jid.to_string())
|
.value(db.real_jid.real_jid, real_jid.to_string())
|
||||||
.perform();
|
.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sp.get_name() == "quoted-item-id") {
|
||||||
|
db.reply.upsert()
|
||||||
|
.value(db.reply.message_id, id, true)
|
||||||
|
.value(db.reply.quoted_content_item_id, quoted_item_id)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,7 @@ public abstract class MetaConversationItem : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ConversationItemWidgetInterface: Object {
|
public interface ConversationItemWidgetInterface: Object {
|
||||||
public abstract void set_widget(Object object, WidgetType type);
|
public abstract void set_widget(Object object, WidgetType type, int priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
collection_conversations.unset(conversation);
|
collection_conversations.unset(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
|
private Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
|
||||||
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func);
|
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func);
|
||||||
|
|
||||||
foreach (var row in select) {
|
foreach (var row in select) {
|
||||||
|
@ -58,7 +58,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
|
private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
|
||||||
switch (content_type) {
|
switch (content_type) {
|
||||||
case 1:
|
case 1:
|
||||||
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
|
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
|
||||||
|
|
|
@ -7,7 +7,7 @@ using Dino.Entities;
|
||||||
namespace Dino {
|
namespace Dino {
|
||||||
|
|
||||||
public class Database : Qlite.Database {
|
public class Database : Qlite.Database {
|
||||||
private const int VERSION = 23;
|
private const int VERSION = 24;
|
||||||
|
|
||||||
public class AccountTable : Table {
|
public class AccountTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
@ -97,6 +97,20 @@ public class Database : Qlite.Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BodyMeta : Table {
|
||||||
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
public Column<int> message_id = new Column.Integer("message_id");
|
||||||
|
public Column<int> from_char = new Column.Integer("from_char");
|
||||||
|
public Column<int> to_char = new Column.Integer("to_char");
|
||||||
|
public Column<string> info_type = new Column.Text("info_type");
|
||||||
|
public Column<string> info = new Column.Text("info");
|
||||||
|
|
||||||
|
internal BodyMeta(Database db) {
|
||||||
|
base(db, "body_meta");
|
||||||
|
init({id, message_id, from_char, to_char, info_type, info});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class MessageCorrectionTable : Table {
|
public class MessageCorrectionTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
public Column<int> message_id = new Column.Integer("message_id") { unique=true };
|
public Column<int> message_id = new Column.Integer("message_id") { unique=true };
|
||||||
|
@ -109,6 +123,20 @@ public class Database : Qlite.Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ReplyTable : Table {
|
||||||
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
public Column<int> message_id = new Column.Integer("message_id") { not_null = true, unique=true };
|
||||||
|
public Column<int> quoted_content_item_id = new Column.Integer("quoted_message_id");
|
||||||
|
public Column<string?> quoted_message_stanza_id = new Column.Text("quoted_message_stanza_id");
|
||||||
|
public Column<string?> quoted_message_from = new Column.Text("quoted_message_from");
|
||||||
|
|
||||||
|
internal ReplyTable(Database db) {
|
||||||
|
base(db, "reply");
|
||||||
|
init({id, message_id, quoted_content_item_id, quoted_message_stanza_id, quoted_message_from});
|
||||||
|
index("reply_quoted_message_stanza_id", {quoted_message_stanza_id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RealJidTable : Table {
|
public class RealJidTable : Table {
|
||||||
public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
|
public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
|
||||||
public Column<string> real_jid = new Column.Text("real_jid");
|
public Column<string> real_jid = new Column.Text("real_jid");
|
||||||
|
@ -337,6 +365,8 @@ public class Database : Qlite.Database {
|
||||||
public EntityTable entity { get; private set; }
|
public EntityTable entity { get; private set; }
|
||||||
public ContentItemTable content_item { get; private set; }
|
public ContentItemTable content_item { get; private set; }
|
||||||
public MessageTable message { get; private set; }
|
public MessageTable message { get; private set; }
|
||||||
|
public BodyMeta body_meta { get; private set; }
|
||||||
|
public ReplyTable reply { get; private set; }
|
||||||
public MessageCorrectionTable message_correction { get; private set; }
|
public MessageCorrectionTable message_correction { get; private set; }
|
||||||
public RealJidTable real_jid { get; private set; }
|
public RealJidTable real_jid { get; private set; }
|
||||||
public OccupantIdTable occupantid { get; private set; }
|
public OccupantIdTable occupantid { get; private set; }
|
||||||
|
@ -364,7 +394,9 @@ public class Database : Qlite.Database {
|
||||||
entity = new EntityTable(this);
|
entity = new EntityTable(this);
|
||||||
content_item = new ContentItemTable(this);
|
content_item = new ContentItemTable(this);
|
||||||
message = new MessageTable(this);
|
message = new MessageTable(this);
|
||||||
|
body_meta = new BodyMeta(this);
|
||||||
message_correction = new MessageCorrectionTable(this);
|
message_correction = new MessageCorrectionTable(this);
|
||||||
|
reply = new ReplyTable(this);
|
||||||
occupantid = new OccupantIdTable(this);
|
occupantid = new OccupantIdTable(this);
|
||||||
real_jid = new RealJidTable(this);
|
real_jid = new RealJidTable(this);
|
||||||
file_transfer = new FileTransferTable(this);
|
file_transfer = new FileTransferTable(this);
|
||||||
|
@ -379,7 +411,7 @@ public class Database : Qlite.Database {
|
||||||
reaction = new ReactionTable(this);
|
reaction = new ReactionTable(this);
|
||||||
settings = new SettingsTable(this);
|
settings = new SettingsTable(this);
|
||||||
conversation_settings = new ConversationSettingsTable(this);
|
conversation_settings = new ConversationSettingsTable(this);
|
||||||
init({ account, jid, entity, content_item, message, message_correction, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings });
|
init({ account, jid, entity, content_item, message, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
exec("PRAGMA journal_mode = WAL");
|
exec("PRAGMA journal_mode = WAL");
|
||||||
|
|
67
libdino/src/service/fallback_body.vala
Normal file
67
libdino/src/service/fallback_body.vala
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
using Gee;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
public class Dino.FallbackBody : StreamInteractionModule, Object {
|
||||||
|
public static ModuleIdentity<FallbackBody> IDENTITY = new ModuleIdentity<FallbackBody>("fallback-body");
|
||||||
|
public string id { get { return IDENTITY.id; } }
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
private ReceivedMessageListener received_message_listener;
|
||||||
|
|
||||||
|
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||||
|
FallbackBody m = new FallbackBody(stream_interactor, db);
|
||||||
|
stream_interactor.add_module(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FallbackBody(StreamInteractor stream_interactor, Database db) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
this.received_message_listener = new ReceivedMessageListener(stream_interactor, db);
|
||||||
|
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReceivedMessageListener : MessageListener {
|
||||||
|
|
||||||
|
public string[] after_actions_const = new string[]{ "STORE" };
|
||||||
|
public override string action_group { get { return "Quote"; } }
|
||||||
|
public override string[] after_actions { get { return after_actions_const; } }
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
|
Gee.List<Xep.FallbackIndication.Fallback> fallbacks = Xep.FallbackIndication.get_fallbacks(stanza);
|
||||||
|
if (fallbacks.is_empty) return false;
|
||||||
|
|
||||||
|
foreach (var fallback in fallbacks) {
|
||||||
|
if (fallback.ns_uri != Xep.Replies.NS_URI) continue;
|
||||||
|
|
||||||
|
foreach (var location in fallback.locations) {
|
||||||
|
db.body_meta.insert()
|
||||||
|
.value(db.body_meta.message_id, message.id)
|
||||||
|
.value(db.body_meta.info_type, Xep.FallbackIndication.NS_URI)
|
||||||
|
.value(db.body_meta.info, fallback.ns_uri)
|
||||||
|
.value(db.body_meta.from_char, location.from_char)
|
||||||
|
.value(db.body_meta.to_char, location.to_char)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
message.set_fallbacks(fallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ public class MessageCorrection : StreamInteractionModule, MessageListener {
|
||||||
|
|
||||||
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation);
|
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation);
|
||||||
out_message.edit_to = stanza_id;
|
out_message.edit_to = stanza_id;
|
||||||
|
out_message.quoted_item_id = old_message.quoted_item_id;
|
||||||
outstanding_correction_nodes[out_message.stanza_id] = stanza_id;
|
outstanding_correction_nodes[out_message.stanza_id] = stanza_id;
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation);
|
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation);
|
||||||
|
|
||||||
|
|
|
@ -424,6 +424,26 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
} else {
|
} else {
|
||||||
new_message.type_ = MessageStanza.TYPE_CHAT;
|
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.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 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
build_message_stanza(message, new_message, conversation);
|
build_message_stanza(message, new_message, conversation);
|
||||||
pre_message_send(message, new_message, conversation);
|
pre_message_send(message, new_message, conversation);
|
||||||
if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return;
|
if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return;
|
||||||
|
|
|
@ -42,6 +42,7 @@ public class MessageStorage : StreamInteractionModule, Object {
|
||||||
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
||||||
.order_by(db.message.time, "DESC")
|
.order_by(db.message.time, "DESC")
|
||||||
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
|
.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)
|
||||||
.limit(count);
|
.limit(count);
|
||||||
|
|
||||||
Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func);
|
Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func);
|
||||||
|
@ -92,6 +93,7 @@ public class MessageStorage : StreamInteractionModule, Object {
|
||||||
|
|
||||||
RowOption row_option = db.message.select().with(db.message.id, "=", id)
|
RowOption row_option = db.message.select().with(db.message.id, "=", id)
|
||||||
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
|
.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)
|
||||||
.row();
|
.row();
|
||||||
|
|
||||||
return create_message_from_row_opt(row_option, conversation);
|
return create_message_from_row_opt(row_option, conversation);
|
||||||
|
@ -111,7 +113,8 @@ public class MessageStorage : StreamInteractionModule, Object {
|
||||||
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
||||||
.with(db.message.stanza_id, "=", stanza_id)
|
.with(db.message.stanza_id, "=", stanza_id)
|
||||||
.order_by(db.message.time, "DESC")
|
.order_by(db.message.time, "DESC")
|
||||||
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id);
|
.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) {
|
if (conversation.counterpart.resourcepart == null) {
|
||||||
query.with_null(db.message.counterpart_resource);
|
query.with_null(db.message.counterpart_resource);
|
||||||
|
@ -138,7 +141,8 @@ public class MessageStorage : StreamInteractionModule, Object {
|
||||||
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
|
||||||
.with(db.message.server_id, "=", server_id)
|
.with(db.message.server_id, "=", server_id)
|
||||||
.order_by(db.message.time, "DESC")
|
.order_by(db.message.time, "DESC")
|
||||||
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id);
|
.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) {
|
if (conversation.counterpart.resourcepart == null) {
|
||||||
query.with_null(db.message.counterpart_resource);
|
query.with_null(db.message.counterpart_resource);
|
||||||
|
|
130
libdino/src/service/replies.vala
Normal file
130
libdino/src/service/replies.vala
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
using Gee;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
public class Dino.Replies : StreamInteractionModule, Object {
|
||||||
|
public static ModuleIdentity<Replies> IDENTITY = new ModuleIdentity<Replies>("reply");
|
||||||
|
public string id { get { return IDENTITY.id; } }
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
private HashMap<Conversation, HashMap<string, Gee.List<Message>>> unmapped_replies = new HashMap<Conversation, HashMap<string, Gee.List<Message>>>();
|
||||||
|
|
||||||
|
private ReceivedMessageListener received_message_listener;
|
||||||
|
|
||||||
|
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||||
|
Replies m = new Replies(stream_interactor, db);
|
||||||
|
stream_interactor.add_module(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Replies(StreamInteractor stream_interactor, Database db) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
this.received_message_listener = new ReceivedMessageListener(stream_interactor, this);
|
||||||
|
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentItem? get_quoted_content_item(Message message, Conversation conversation) {
|
||||||
|
if (message.quoted_item_id == 0) return null;
|
||||||
|
|
||||||
|
RowOption row_option = db.reply.select().with(db.reply.message_id, "=", message.id).row();
|
||||||
|
if (row_option.is_present()) {
|
||||||
|
return stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, row_option[db.reply.quoted_content_item_id]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_message_is_reply_to(Message message, ContentItem reply_to) {
|
||||||
|
message.quoted_item_id = reply_to.id;
|
||||||
|
|
||||||
|
db.reply.upsert()
|
||||||
|
.value(db.reply.message_id, message.id, true)
|
||||||
|
.value(db.reply.quoted_content_item_id, reply_to.id)
|
||||||
|
.value_null(db.reply.quoted_message_stanza_id)
|
||||||
|
.value_null(db.reply.quoted_message_from)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
|
// Check if a previous message was in reply to this one
|
||||||
|
string relevant_id = conversation.type_ == Conversation.Type.GROUPCHAT ? message.server_id : message.stanza_id;
|
||||||
|
|
||||||
|
var reply_qry = db.reply.select();
|
||||||
|
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
|
||||||
|
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id);
|
||||||
|
} else {
|
||||||
|
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.stanza_id);
|
||||||
|
}
|
||||||
|
reply_qry.join_with(db.message, db.reply.message_id, db.message.id)
|
||||||
|
.with(db.message.account_id, "=", conversation.account.id)
|
||||||
|
.with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart))
|
||||||
|
.with(db.message.time, ">", (long)message.time.to_unix())
|
||||||
|
.order_by(db.message.time, "DESC");
|
||||||
|
|
||||||
|
foreach (Row reply_row in reply_qry) {
|
||||||
|
ContentItem? message_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
|
||||||
|
Message? reply_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(reply_row[db.message.id], conversation);
|
||||||
|
if (message_item != null && reply_message != null) {
|
||||||
|
set_message_is_reply_to(reply_message, message_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle if this message is a reply
|
||||||
|
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);
|
||||||
|
if (quoted_content_item == null) return;
|
||||||
|
|
||||||
|
set_message_is_reply_to(message, quoted_content_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReceivedMessageListener : MessageListener {
|
||||||
|
|
||||||
|
public string[] after_actions_const = new string[]{ "STORE", "STORE_CONTENT_ITEM" };
|
||||||
|
public override string action_group { get { return "Quote"; } }
|
||||||
|
public override string[] after_actions { get { return after_actions_const; } }
|
||||||
|
|
||||||
|
private Replies outer;
|
||||||
|
|
||||||
|
public ReceivedMessageListener(StreamInteractor stream_interactor, Replies outer) {
|
||||||
|
this.outer = outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
|
outer.on_incoming_message(message, stanza, conversation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Dino {
|
||||||
|
public string message_body_without_reply_fallback(Message message) {
|
||||||
|
string body = message.body;
|
||||||
|
foreach (var fallback in message.get_fallbacks()) {
|
||||||
|
if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) {
|
||||||
|
body = body[0:fallback.locations[0].from_char] + body[fallback.locations[0].to_char:body.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,7 @@ set(RESOURCE_LIST
|
||||||
message_item_widget_edit_mode.ui
|
message_item_widget_edit_mode.ui
|
||||||
occupant_list.ui
|
occupant_list.ui
|
||||||
occupant_list_item.ui
|
occupant_list_item.ui
|
||||||
|
quote.ui
|
||||||
search_autocomplete.ui
|
search_autocomplete.ui
|
||||||
settings_dialog.ui
|
settings_dialog.ui
|
||||||
shortcuts.ui
|
shortcuts.ui
|
||||||
|
@ -157,6 +158,7 @@ SOURCES
|
||||||
src/ui/conversation_content_view/file_image_widget.vala
|
src/ui/conversation_content_view/file_image_widget.vala
|
||||||
src/ui/conversation_content_view/file_widget.vala
|
src/ui/conversation_content_view/file_widget.vala
|
||||||
src/ui/conversation_content_view/message_widget.vala
|
src/ui/conversation_content_view/message_widget.vala
|
||||||
|
src/ui/conversation_content_view/quote_widget.vala
|
||||||
src/ui/conversation_content_view/reactions_widget.vala
|
src/ui/conversation_content_view/reactions_widget.vala
|
||||||
src/ui/conversation_content_view/subscription_notification.vala
|
src/ui/conversation_content_view/subscription_notification.vala
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,24 @@
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="quote_box">
|
||||||
|
<property name="margin-top">10</property>
|
||||||
|
<property name="margin-start">10</property>
|
||||||
|
<property name="margin-end">10</property>
|
||||||
|
<property name="visible">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="DinoUiChatTextView" id="chat_text_view">
|
<object class="DinoUiChatTextView" id="chat_text_view">
|
||||||
<property name="margin_start">7</property>
|
<property name="margin_start">7</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkMenuButton" id="emoji_button">
|
<object class="GtkMenuButton" id="emoji_button">
|
||||||
<property name="icon-name">dino-emoticon-symbolic</property>
|
<property name="icon-name">dino-emoticon-symbolic</property>
|
||||||
|
|
|
@ -3,12 +3,6 @@
|
||||||
<requires lib="gtk" version="4.0"/>
|
<requires lib="gtk" version="4.0"/>
|
||||||
<template class="DinoUiConversationSummaryConversationView">
|
<template class="DinoUiConversationSummaryConversationView">
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkStack" id="stack">
|
|
||||||
<property name="transition_type">crossfade</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkStackPage">
|
|
||||||
<property name="name">main</property>
|
|
||||||
<property name="child">
|
|
||||||
<object class="GtkOverlay">
|
<object class="GtkOverlay">
|
||||||
<property name="child">
|
<property name="child">
|
||||||
<object class="GtkScrolledWindow" id="scrolled">
|
<object class="GtkScrolledWindow" id="scrolled">
|
||||||
|
@ -39,19 +33,6 @@
|
||||||
<style>
|
<style>
|
||||||
<class name="linked"/>
|
<class name="linked"/>
|
||||||
</style>
|
</style>
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="button1">
|
|
||||||
<property name="visible">0</property>
|
|
||||||
<property name="vexpand">0</property>
|
|
||||||
<property name="halign">end</property>
|
|
||||||
<property name="valign">end</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="button1_icon">
|
|
||||||
<property name="icon-size">normal</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -84,18 +65,6 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkStackPage">
|
|
||||||
<property name="name">void</property>
|
|
||||||
<property name="child">
|
|
||||||
<object class="GtkBox"/>
|
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
</child>
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
80
main/data/quote.ui
Normal file
80
main/data/quote.ui
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
|
<object class="GtkGrid" id="outer">
|
||||||
|
<property name="column-spacing">5</property>
|
||||||
|
<style>
|
||||||
|
<class name="dino-quote"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="DinoUiAvatarImage" id="avatar">
|
||||||
|
<property name="allow_gray">False</property>
|
||||||
|
<property name="height">15</property>
|
||||||
|
<property name="width">15</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<layout>
|
||||||
|
<property name="column">0</property>
|
||||||
|
<property name="row">0</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="author">
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="valign">baseline</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
|
||||||
|
</attributes>
|
||||||
|
<layout>
|
||||||
|
<property name="column">1</property>
|
||||||
|
<property name="row">0</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="time">
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="valign">baseline</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="scale" value="0.8"/>
|
||||||
|
</attributes>
|
||||||
|
<layout>
|
||||||
|
<property name="column">2</property>
|
||||||
|
<property name="row">0</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="message">
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<layout>
|
||||||
|
<property name="column">0</property>
|
||||||
|
<property name="row">1</property>
|
||||||
|
<property name="column-span">3</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="abort-button">
|
||||||
|
<property name="icon-name">window-close-symbolic</property>
|
||||||
|
<property name="has-frame">False</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<layout>
|
||||||
|
<property name="column">3</property>
|
||||||
|
<property name="row">0</property>
|
||||||
|
<property name="row-span">2</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
|
@ -109,6 +109,17 @@ window.dino-main .dino-conversation .message-box.error:hover {
|
||||||
background: alpha(@error_color, 0.12);
|
background: alpha(@error_color, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.dino-main .dino-quote {
|
||||||
|
border-left: 3px solid alpha(@theme_fg_color, 0.2);
|
||||||
|
background: alpha(@theme_fg_color, 0.05);
|
||||||
|
border-color: alpha(@theme_fg_color, 0.2);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main .dino-quote:hover {
|
||||||
|
background: alpha(@theme_fg_color, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* Message Menu */
|
/* Message Menu */
|
||||||
|
|
||||||
.message-menu-box {
|
.message-menu-box {
|
||||||
|
|
|
@ -24,6 +24,8 @@ public class ChatInputController : Object {
|
||||||
private Plugins.InputFieldStatus input_field_status;
|
private Plugins.InputFieldStatus input_field_status;
|
||||||
private ChatTextViewController chat_text_view_controller;
|
private ChatTextViewController chat_text_view_controller;
|
||||||
|
|
||||||
|
private ContentItem? quoted_content_item = null;
|
||||||
|
|
||||||
public ChatInputController(ChatInput.View chat_input, StreamInteractor stream_interactor) {
|
public ChatInputController(ChatInput.View chat_input, StreamInteractor stream_interactor) {
|
||||||
this.chat_input = chat_input;
|
this.chat_input = chat_input;
|
||||||
this.status_description_label = chat_input.chat_input_status;
|
this.status_description_label = chat_input.chat_input_status;
|
||||||
|
@ -58,12 +60,34 @@ public class ChatInputController : Object {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SimpleAction quote_action = new SimpleAction("quote", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32}));
|
||||||
|
quote_action.activate.connect((variant) => {
|
||||||
|
int conversation_id = variant.get_child_value(0).get_int32();
|
||||||
|
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id);
|
||||||
|
if (conversation == null || !this.conversation.equals(conversation)) return;
|
||||||
|
|
||||||
|
int content_item_id = variant.get_child_value(1).get_int32();
|
||||||
|
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, content_item_id);
|
||||||
|
if (content_item == null) return;
|
||||||
|
|
||||||
|
quoted_content_item = content_item;
|
||||||
|
var quote_model = new Quote.Model.from_content_item(content_item, conversation, stream_interactor) { can_abort = true };
|
||||||
|
quote_model.aborted.connect(() => {
|
||||||
|
content_item = null;
|
||||||
|
chat_input.unset_quoted_message();
|
||||||
|
});
|
||||||
|
chat_input.set_quoted_message(Quote.get_widget(quote_model));
|
||||||
|
});
|
||||||
|
GLib.Application.get_default().add_action(quote_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set_conversation(Conversation conversation) {
|
public void set_conversation(Conversation conversation) {
|
||||||
this.conversation = conversation;
|
this.quoted_content_item = null;
|
||||||
|
|
||||||
reset_input_field_status();
|
reset_input_field_status();
|
||||||
|
chat_input.unset_quoted_message();
|
||||||
|
|
||||||
|
this.conversation = conversation;
|
||||||
|
|
||||||
chat_input.encryption_widget.set_conversation(conversation);
|
chat_input.encryption_widget.set_conversation(conversation);
|
||||||
|
|
||||||
|
@ -111,7 +135,10 @@ public class ChatInputController : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
string text = chat_input.chat_text_view.text_view.buffer.text;
|
string text = chat_input.chat_text_view.text_view.buffer.text;
|
||||||
|
|
||||||
chat_input.chat_text_view.text_view.buffer.text = "";
|
chat_input.chat_text_view.text_view.buffer.text = "";
|
||||||
|
chat_input.unset_quoted_message();
|
||||||
|
|
||||||
if (text.has_prefix("/")) {
|
if (text.has_prefix("/")) {
|
||||||
string[] token = text.split(" ", 2);
|
string[] token = text.split(" ", 2);
|
||||||
switch(token[0]) {
|
switch(token[0]) {
|
||||||
|
@ -164,7 +191,11 @@ public class ChatInputController : Object {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).send_text(text, conversation);
|
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation);
|
||||||
|
if (quoted_content_item != null) {
|
||||||
|
stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item);
|
||||||
|
}
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_text_input_changed() {
|
private void on_text_input_changed() {
|
||||||
|
|
|
@ -20,8 +20,8 @@ public class View : Box {
|
||||||
private HashMap<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func);
|
private HashMap<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func);
|
||||||
|
|
||||||
[GtkChild] public unowned Frame frame;
|
[GtkChild] public unowned Frame frame;
|
||||||
|
[GtkChild] public unowned Box quote_box;
|
||||||
[GtkChild] public unowned ChatTextView chat_text_view;
|
[GtkChild] public unowned ChatTextView chat_text_view;
|
||||||
[GtkChild] public unowned Box outer_box;
|
|
||||||
[GtkChild] public unowned Button file_button;
|
[GtkChild] public unowned Button file_button;
|
||||||
[GtkChild] public unowned MenuButton emoji_button;
|
[GtkChild] public unowned MenuButton emoji_button;
|
||||||
[GtkChild] public unowned MenuButton encryption_button;
|
[GtkChild] public unowned MenuButton encryption_button;
|
||||||
|
@ -94,6 +94,19 @@ public class View : Box {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void set_quoted_message(Widget quote_widget) {
|
||||||
|
Widget? quote_box_child = quote_box.get_first_child();
|
||||||
|
if (quote_box_child != null) quote_box.remove(quote_box_child);
|
||||||
|
quote_box.append(quote_widget);
|
||||||
|
quote_box.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unset_quoted_message() {
|
||||||
|
Widget? quote_box_child = quote_box.get_first_child();
|
||||||
|
if (quote_box_child != null) quote_box.remove(quote_box_child);
|
||||||
|
quote_box.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
public void do_focus() {
|
public void do_focus() {
|
||||||
chat_text_view.text_view.grab_focus();
|
chat_text_view.text_view.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
public Image encryption_image { get; set; }
|
public Image encryption_image { get; set; }
|
||||||
public Image received_image { get; set; }
|
public Image received_image { get; set; }
|
||||||
|
|
||||||
public Widget? content_widget = null;
|
private HashMap<int, Widget> content_widgets = new HashMap<int, Widget>();
|
||||||
|
|
||||||
private bool show_skeleton_ = false;
|
private bool show_skeleton_ = false;
|
||||||
public bool show_skeleton {
|
public bool show_skeleton {
|
||||||
|
@ -58,7 +58,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
widget = item.get_widget(this, Plugins.WidgetType.GTK4) as Widget;
|
widget = item.get_widget(this, Plugins.WidgetType.GTK4) as Widget;
|
||||||
if (widget != null) {
|
if (widget != null) {
|
||||||
widget.valign = Align.END;
|
widget.valign = Align.END;
|
||||||
set_widget(widget, Plugins.WidgetType.GTK4);
|
set_widget(widget, Plugins.WidgetType.GTK4, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.requires_header) {
|
if (item.requires_header) {
|
||||||
|
@ -72,7 +72,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
if (content_meta_item != null) {
|
if (content_meta_item != null) {
|
||||||
reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor);
|
reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor);
|
||||||
reactions_controller.box_activated.connect((widget) => {
|
reactions_controller.box_activated.connect((widget) => {
|
||||||
main_grid.attach(widget, 1, 2, 4, 1);
|
set_widget(widget, Plugins.WidgetType.GTK4, 3);
|
||||||
});
|
});
|
||||||
reactions_controller.init();
|
reactions_controller.init();
|
||||||
}
|
}
|
||||||
|
@ -103,12 +103,18 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
update_received_mark();
|
update_received_mark();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set_widget(Object object, Plugins.WidgetType type) {
|
public void set_widget(Object object, Plugins.WidgetType type, int priority) {
|
||||||
if (content_widget != null) content_widget.unparent();
|
foreach (var content_widget in content_widgets.values) {
|
||||||
|
content_widget.unparent();
|
||||||
|
}
|
||||||
|
|
||||||
Widget widget = (Widget) object;
|
content_widgets[priority] = (Widget) object;
|
||||||
content_widget = widget;
|
int row_no = 1;
|
||||||
main_grid.attach(widget, 1, 1, 4, 1);
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (!content_widgets.has_key(i)) continue;
|
||||||
|
main_grid.attach(content_widgets[i], 1, row_no, 4, 1);
|
||||||
|
row_no++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update_margin() {
|
private void update_margin() {
|
||||||
|
|
|
@ -18,7 +18,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
[GtkChild] private unowned Box notifications;
|
[GtkChild] private unowned Box notifications;
|
||||||
[GtkChild] private unowned Box main;
|
[GtkChild] private unowned Box main;
|
||||||
[GtkChild] private unowned Box main_wrap_box;
|
[GtkChild] private unowned Box main_wrap_box;
|
||||||
[GtkChild] private unowned Stack stack;
|
|
||||||
|
|
||||||
private ArrayList<Widget> action_buttons = new ArrayList<Widget>();
|
private ArrayList<Widget> action_buttons = new ArrayList<Widget>();
|
||||||
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
|
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
|
||||||
|
@ -208,7 +207,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
Button button = new Button();
|
Button button = new Button();
|
||||||
button.icon_name = message_action.icon_name;
|
button.icon_name = message_action.icon_name;
|
||||||
button.clicked.connect(() => {
|
button.clicked.connect(() => {
|
||||||
print(@"$(current_meta_item.jid) skdfj \n");
|
|
||||||
message_action.callback(button, current_meta_item, currently_highlighted);
|
message_action.callback(button, current_meta_item, currently_highlighted);
|
||||||
});
|
});
|
||||||
action_buttons.add(button);
|
action_buttons.add(button);
|
||||||
|
@ -233,15 +231,12 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
});
|
});
|
||||||
firstLoad = false;
|
firstLoad = false;
|
||||||
}
|
}
|
||||||
stack.set_visible_child_name("void");
|
|
||||||
clear();
|
clear();
|
||||||
initialize_for_conversation_(conversation);
|
initialize_for_conversation_(conversation);
|
||||||
display_latest();
|
display_latest();
|
||||||
stack.set_visible_child_name("main");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize_around_message(Conversation conversation, ContentItem content_item) {
|
public void initialize_around_message(Conversation conversation, ContentItem content_item) {
|
||||||
stack.set_visible_child_name("void");
|
|
||||||
clear();
|
clear();
|
||||||
initialize_for_conversation_(conversation);
|
initialize_for_conversation_(conversation);
|
||||||
Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
|
Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
|
||||||
|
@ -277,7 +272,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
|
scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
|
||||||
w.add_css_class("highlight-once");
|
w.add_css_class("highlight-once");
|
||||||
reload_messages = true;
|
reload_messages = true;
|
||||||
stack.set_visible_child_name("main");
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,8 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
|
|
||||||
bool theme_dependent = false;
|
bool theme_dependent = false;
|
||||||
|
|
||||||
string markup_text = message.body;
|
string markup_text = Dino.message_body_without_reply_fallback(message);
|
||||||
|
|
||||||
if (markup_text.length > 10000) {
|
if (markup_text.length > 10000) {
|
||||||
markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]";
|
markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]";
|
||||||
}
|
}
|
||||||
|
@ -169,7 +170,7 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
|
|
||||||
edit_mode.cancelled.connect(() => {
|
edit_mode.cancelled.connect(() => {
|
||||||
in_edit_mode = false;
|
in_edit_mode = false;
|
||||||
outer.set_widget(label, Plugins.WidgetType.GTK4);
|
outer.set_widget(label, Plugins.WidgetType.GTK4, 2);
|
||||||
});
|
});
|
||||||
edit_mode.send.connect(() => {
|
edit_mode.send.connect(() => {
|
||||||
if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) {
|
if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) {
|
||||||
|
@ -178,18 +179,31 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
// edit_cancelled();
|
// edit_cancelled();
|
||||||
}
|
}
|
||||||
in_edit_mode = false;
|
in_edit_mode = false;
|
||||||
outer.set_widget(label, Plugins.WidgetType.GTK4);
|
outer.set_widget(label, Plugins.WidgetType.GTK4, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
edit_mode.chat_text_view.text_view.buffer.text = message.body;
|
edit_mode.chat_text_view.text_view.buffer.text = message.body;
|
||||||
|
|
||||||
outer.set_widget(edit_mode, Plugins.WidgetType.GTK4);
|
outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2);
|
||||||
edit_mode.chat_text_view.text_view.grab_focus();
|
edit_mode.chat_text_view.text_view.grab_focus();
|
||||||
} else {
|
} else {
|
||||||
this.in_edit_mode = false;
|
this.in_edit_mode = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
outer.set_widget(label, Plugins.WidgetType.GTK4, 2);
|
||||||
|
|
||||||
|
if (message_item.message.quoted_item_id > 0) {
|
||||||
|
var quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(message_item.conversation, message_item.message.quoted_item_id);
|
||||||
|
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) }));
|
||||||
|
});
|
||||||
|
var quote_widget = Quote.get_widget(quote_model);
|
||||||
|
outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,6 +223,13 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
actions.add(action1);
|
actions.add(action1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }));
|
||||||
|
};
|
||||||
|
actions.add(reply_action);
|
||||||
|
|
||||||
if (supports_reaction) {
|
if (supports_reaction) {
|
||||||
Plugins.MessageAction action2 = new Plugins.MessageAction();
|
Plugins.MessageAction action2 = new Plugins.MessageAction();
|
||||||
action2.icon_name = "dino-emoticon-add-symbolic";
|
action2.icon_name = "dino-emoticon-add-symbolic";
|
||||||
|
|
73
main/src/ui/conversation_content_view/quote_widget.vala
Normal file
73
main/src/ui/conversation_content_view/quote_widget.vala
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
using Dino.Ui.ConversationSummary;
|
||||||
|
using Gee;
|
||||||
|
using Gtk;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino.Ui.Quote {
|
||||||
|
|
||||||
|
public class Model : Object {
|
||||||
|
public signal void aborted();
|
||||||
|
public signal void jump_to();
|
||||||
|
|
||||||
|
public string display_name { get; set; }
|
||||||
|
public string message { get; set; }
|
||||||
|
public DateTime message_time { get; set; }
|
||||||
|
|
||||||
|
public StreamInteractor stream_interactor { get; set; }
|
||||||
|
public Conversation conversation { get; set; }
|
||||||
|
public Jid author_jid { get; set; }
|
||||||
|
|
||||||
|
public bool can_abort { get; set; default=false; }
|
||||||
|
|
||||||
|
public Model.from_content_item(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) {
|
||||||
|
this.display_name = Util.get_participant_display_name(stream_interactor, conversation, content_item.jid, true);
|
||||||
|
if (content_item.type_ == MessageItem.TYPE) {
|
||||||
|
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]";
|
||||||
|
}
|
||||||
|
this.message_time = content_item.time;
|
||||||
|
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.conversation = conversation;
|
||||||
|
this.author_jid = content_item.jid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Widget get_widget(Model model) {
|
||||||
|
Builder builder = new Builder.from_resource("/im/dino/Dino/quote.ui");
|
||||||
|
AvatarImage avatar = (AvatarImage) builder.get_object("avatar");
|
||||||
|
Label author = (Label) builder.get_object("author");
|
||||||
|
Label time = (Label) builder.get_object("time");
|
||||||
|
Label message = (Label) builder.get_object("message");
|
||||||
|
Button abort_button = (Button) builder.get_object("abort-button");
|
||||||
|
|
||||||
|
avatar.set_conversation_participant(model.stream_interactor, model.conversation, model.author_jid);
|
||||||
|
model.bind_property("display-name", author, "label", BindingFlags.SYNC_CREATE);
|
||||||
|
model.bind_property("message-time", time, "label", BindingFlags.SYNC_CREATE, (_, from_value, ref to_value) => {
|
||||||
|
DateTime message_time = (DateTime) from_value;
|
||||||
|
to_value = ConversationItemSkeleton.get_relative_time(message_time);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
model.bind_property("message", message, "label", BindingFlags.SYNC_CREATE);
|
||||||
|
model.bind_property("can-abort", abort_button, "visible", BindingFlags.SYNC_CREATE);
|
||||||
|
|
||||||
|
abort_button.clicked.connect(() => {
|
||||||
|
model.aborted();
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget outer = builder.get_object("outer") as Widget;
|
||||||
|
|
||||||
|
GestureClick gesture_click_controller = new GestureClick();
|
||||||
|
outer.add_controller(gesture_click_controller);
|
||||||
|
gesture_click_controller.pressed.connect(() => {
|
||||||
|
model.jump_to();
|
||||||
|
});
|
||||||
|
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,9 +27,6 @@ public class ReactionsController : Object {
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
Gee.List<ReactionUsers> reactions = stream_interactor.get_module(Reactions.IDENTITY).get_item_reactions(conversation, content_item);
|
Gee.List<ReactionUsers> reactions = stream_interactor.get_module(Reactions.IDENTITY).get_item_reactions(conversation, content_item);
|
||||||
if (reactions.size > 0) {
|
|
||||||
initialize_widget();
|
|
||||||
}
|
|
||||||
foreach (ReactionUsers reaction_users in reactions) {
|
foreach (ReactionUsers reaction_users in reactions) {
|
||||||
foreach (Jid jid in reaction_users.jids) {
|
foreach (Jid jid in reaction_users.jids) {
|
||||||
reaction_added(reaction_users.reaction, jid);
|
reaction_added(reaction_users.reaction, jid);
|
||||||
|
|
|
@ -149,7 +149,7 @@ public class ConversationSelectorRow : ListBoxRow {
|
||||||
MessageItem message_item = last_content_item as MessageItem;
|
MessageItem message_item = last_content_item as MessageItem;
|
||||||
Message last_message = message_item.message;
|
Message last_message = message_item.message;
|
||||||
|
|
||||||
string body = last_message.body;
|
string body = Dino.message_body_without_reply_fallback(last_message);
|
||||||
bool me_command = body.has_prefix("/me ");
|
bool me_command = body.has_prefix("/me ");
|
||||||
|
|
||||||
/* If we have a /me command, we always show the display
|
/* If we have a /me command, we always show the display
|
||||||
|
|
|
@ -23,6 +23,21 @@ public class MainWindowController : Object {
|
||||||
|
|
||||||
stream_interactor.get_module(ConversationManager.IDENTITY).conversation_deactivated.connect(check_unset_conversation);
|
stream_interactor.get_module(ConversationManager.IDENTITY).conversation_deactivated.connect(check_unset_conversation);
|
||||||
stream_interactor.account_removed.connect(check_unset_conversation);
|
stream_interactor.account_removed.connect(check_unset_conversation);
|
||||||
|
|
||||||
|
SimpleAction jump_to_conversatio_message_action = new SimpleAction("jump-to-conversation-message", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32}));
|
||||||
|
jump_to_conversatio_message_action.activate.connect((variant) => {
|
||||||
|
int conversation_id = variant.get_child_value(0).get_int32();
|
||||||
|
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id);
|
||||||
|
if (conversation == null || !this.conversation.equals(conversation)) return;
|
||||||
|
|
||||||
|
int item_id = variant.get_child_value(1).get_int32();
|
||||||
|
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, item_id);
|
||||||
|
|
||||||
|
select_conversation(conversation, false, false);
|
||||||
|
window.conversation_view.conversation_frame.initialize_around_message(conversation, content_item);
|
||||||
|
});
|
||||||
|
app.add_action(jump_to_conversatio_message_action);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set_window(MainWindow window) {
|
public void set_window(MainWindow window) {
|
||||||
|
|
|
@ -133,7 +133,9 @@ SOURCES
|
||||||
"src/module/xep/0391_jingle_encrypted_transports.vala"
|
"src/module/xep/0391_jingle_encrypted_transports.vala"
|
||||||
"src/module/xep/0410_muc_self_ping.vala"
|
"src/module/xep/0410_muc_self_ping.vala"
|
||||||
"src/module/xep/0421_occupant_ids.vala"
|
"src/module/xep/0421_occupant_ids.vala"
|
||||||
|
"src/module/xep/0428_fallback_indication.vala"
|
||||||
"src/module/xep/0444_reactions.vala"
|
"src/module/xep/0444_reactions.vala"
|
||||||
|
"src/module/xep/0461_replies.vala"
|
||||||
"src/module/xep/pixbuf_storage.vala"
|
"src/module/xep/pixbuf_storage.vala"
|
||||||
|
|
||||||
"src/util.vala"
|
"src/util.vala"
|
||||||
|
|
67
xmpp-vala/src/module/xep/0428_fallback_indication.vala
Normal file
67
xmpp-vala/src/module/xep/0428_fallback_indication.vala
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
namespace Xmpp.Xep.FallbackIndication {
|
||||||
|
|
||||||
|
public const string NS_URI = "urn:xmpp:fallback:0";
|
||||||
|
|
||||||
|
public class Fallback {
|
||||||
|
public string ns_uri { get; set; }
|
||||||
|
public FallbackLocation[] locations;
|
||||||
|
|
||||||
|
|
||||||
|
public Fallback(string ns_uri, FallbackLocation[] locations) {
|
||||||
|
this.ns_uri = ns_uri;
|
||||||
|
this.locations = locations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FallbackLocation {
|
||||||
|
public int from_char { get; set; }
|
||||||
|
public int to_char { get; set; }
|
||||||
|
|
||||||
|
public FallbackLocation(int from_char, int to_char) {
|
||||||
|
this.from_char = from_char;
|
||||||
|
this.to_char = to_char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set_fallback(MessageStanza message, Fallback fallback) {
|
||||||
|
StanzaNode fallback_node = (new StanzaNode.build("fallback", NS_URI))
|
||||||
|
.add_self_xmlns()
|
||||||
|
.put_attribute("for", fallback.ns_uri);
|
||||||
|
foreach (FallbackLocation location in fallback.locations) {
|
||||||
|
fallback_node.put_node(new StanzaNode.build("body", NS_URI)
|
||||||
|
.add_self_xmlns()
|
||||||
|
.put_attribute("start", location.from_char.to_string())
|
||||||
|
.put_attribute("end", location.to_char.to_string()));
|
||||||
|
}
|
||||||
|
message.stanza.put_node(fallback_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<Fallback> get_fallbacks(MessageStanza message) {
|
||||||
|
var ret = new ArrayList<Fallback>();
|
||||||
|
|
||||||
|
Gee.List<StanzaNode> fallback_nodes = message.stanza.get_subnodes("fallback", NS_URI);
|
||||||
|
if (fallback_nodes.is_empty) return ret;
|
||||||
|
|
||||||
|
foreach (StanzaNode fallback_node in fallback_nodes) {
|
||||||
|
string? ns_uri = fallback_node.get_attribute("for");
|
||||||
|
if (ns_uri == null) continue;
|
||||||
|
|
||||||
|
Gee.List<StanzaNode> body_nodes = fallback_node.get_subnodes("body", NS_URI);
|
||||||
|
if (body_nodes.is_empty) continue;
|
||||||
|
|
||||||
|
var locations = new ArrayList<FallbackLocation>();
|
||||||
|
foreach (StanzaNode body_node in body_nodes) {
|
||||||
|
int start_char = body_node.get_attribute_int("start", -1);
|
||||||
|
int end_char = body_node.get_attribute_int("end", -1);
|
||||||
|
if (start_char == -1 || end_char == -1) continue;
|
||||||
|
locations.add(new FallbackLocation(start_char, end_char));
|
||||||
|
}
|
||||||
|
if (locations.is_empty) continue;
|
||||||
|
ret.add(new Fallback(ns_uri, locations.to_array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
41
xmpp-vala/src/module/xep/0461_replies.vala
Normal file
41
xmpp-vala/src/module/xep/0461_replies.vala
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
namespace Xmpp.Xep.Replies {
|
||||||
|
|
||||||
|
public const string NS_URI = "urn:xmpp:reply:0";
|
||||||
|
|
||||||
|
public class ReplyTo {
|
||||||
|
public Jid to_jid { get; set; }
|
||||||
|
public string to_message_id { get; set; }
|
||||||
|
|
||||||
|
public ReplyTo(Jid to_jid, string to_message_id) {
|
||||||
|
this.to_jid = to_jid;
|
||||||
|
this.to_message_id = to_message_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set_reply_to(MessageStanza message, ReplyTo reply_to) {
|
||||||
|
StanzaNode reply_node = (new StanzaNode.build("reply", NS_URI))
|
||||||
|
.add_self_xmlns()
|
||||||
|
.put_attribute("to", reply_to.to_jid.to_string())
|
||||||
|
.put_attribute("id", reply_to.to_message_id);
|
||||||
|
message.stanza.put_node(reply_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplyTo? get_reply_to(MessageStanza message) {
|
||||||
|
StanzaNode? reply_node = message.stanza.get_subnode("reply", NS_URI);
|
||||||
|
if (reply_node == null) return null;
|
||||||
|
|
||||||
|
string? to_str = reply_node.get_attribute("to");
|
||||||
|
if (to_str == null) return null;
|
||||||
|
try {
|
||||||
|
Jid to_jid = new Jid(to_str);
|
||||||
|
|
||||||
|
string? id = reply_node.get_attribute("id");
|
||||||
|
if (id == null) return null;
|
||||||
|
|
||||||
|
return new ReplyTo(to_jid, id);
|
||||||
|
} catch (InvalidJidError e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue