Always display reaction+reply buttons, disable if not possible
This commit is contained in:
parent
10a2bce512
commit
b0b81b88c6
|
@ -153,6 +153,7 @@ public interface ConversationItemWidgetInterface: Object {
|
||||||
|
|
||||||
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
||||||
public class MessageAction : Object {
|
public class MessageAction : Object {
|
||||||
|
public bool sensitive = true;
|
||||||
public string icon_name;
|
public string icon_name;
|
||||||
public string? tooltip;
|
public string? tooltip;
|
||||||
public Object? popover;
|
public Object? popover;
|
||||||
|
|
|
@ -79,22 +79,34 @@ public class EntityInfo : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bool has_feature(Account account, Jid jid, string feature) {
|
public async bool has_feature(Account account, Jid jid, string feature) {
|
||||||
|
int has_feature_cached = has_feature_cached_int(account, jid, feature);
|
||||||
|
if (has_feature_cached != -1) {
|
||||||
|
return has_feature_cached == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, entity_caps_hashes[jid]);
|
||||||
|
if (info_result == null) return false;
|
||||||
|
|
||||||
|
return info_result.features.contains(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool has_feature_cached(Account account, Jid jid, string feature) {
|
||||||
|
return has_feature_cached_int(account, jid, feature) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int has_feature_cached_int(Account account, Jid jid, string feature) {
|
||||||
if (jid_features.has_key(jid)) {
|
if (jid_features.has_key(jid)) {
|
||||||
return jid_features[jid].contains(feature);
|
return jid_features[jid].contains(feature) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? hash = entity_caps_hashes[jid];
|
string? hash = entity_caps_hashes[jid];
|
||||||
if (hash != null) {
|
if (hash != null) {
|
||||||
Gee.List<string>? features = get_stored_features(hash);
|
Gee.List<string>? features = get_stored_features(hash);
|
||||||
if (features != null) {
|
if (features != null) {
|
||||||
return features.contains(feature);
|
return features.contains(feature) ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return -1;
|
||||||
ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, hash);
|
|
||||||
if (info_result == null) return false;
|
|
||||||
|
|
||||||
return info_result.features.contains(feature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_received_available_presence(Account account, Presence.Stanza presence) {
|
private void on_received_available_presence(Account account, Presence.Stanza presence) {
|
||||||
|
|
|
@ -64,4 +64,20 @@ public class Dino.FallbackBody : StreamInteractionModule, Object {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string get_quoted_fallback_body(ContentItem content_item) {
|
||||||
|
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";
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -484,17 +484,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
Xep.Replies.set_reply_to(new_stanza, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id));
|
Xep.Replies.set_reply_to(new_stanza, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
string fallback = "> ";
|
string fallback = FallbackBody.get_quoted_fallback_body(content_item);
|
||||||
|
|
||||||
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;
|
long fallback_length = fallback.length;
|
||||||
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
|
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
|
||||||
|
|
|
@ -57,30 +57,21 @@ public class Dino.Reactions : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bool conversation_supports_reactions(Conversation conversation) {
|
public bool conversation_supports_reactions(Conversation conversation) {
|
||||||
if (conversation.type_ == Conversation.Type.CHAT) {
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
Gee.List<Jid>? resources = stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(conversation.counterpart, conversation.account);
|
|
||||||
if (resources == null) return false;
|
|
||||||
|
|
||||||
foreach (Jid full_jid in resources) {
|
|
||||||
bool? has_feature = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(conversation.account, full_jid, Xep.Reactions.NS_URI);
|
|
||||||
if (has_feature == true) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids)
|
// The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids)
|
||||||
var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
||||||
bool server_supports_sid = (yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
bool server_supports_sid = (entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
||||||
(yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
|
(entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
|
||||||
if (!server_supports_sid) return false;
|
if (!server_supports_sid) return false;
|
||||||
|
|
||||||
bool? supports_occupant_ids = yield entity_info.has_feature(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);
|
bool? supports_occupant_ids = entity_info.has_feature_cached(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);
|
||||||
if (supports_occupant_ids) return true;
|
if (supports_occupant_ids) return true;
|
||||||
|
|
||||||
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
|
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) throws SendError {
|
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) throws SendError {
|
||||||
|
@ -96,7 +87,7 @@ public class Dino.Reactions : StreamInteractionModule, Object {
|
||||||
reactions_module.send_reaction.begin(stream, conversation.counterpart, "groupchat", message_id, reactions);
|
reactions_module.send_reaction.begin(stream, conversation.counterpart, "groupchat", message_id, reactions);
|
||||||
// We save the reaction when it gets reflected back to us
|
// We save the reaction when it gets reflected back to us
|
||||||
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
|
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
|
||||||
reactions_module.send_reaction(stream, conversation.counterpart, "chat", message_id, reactions);
|
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions);
|
||||||
} else if (conversation.type_ == Conversation.Type.CHAT) {
|
} else if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
int64 now_millis = GLib.get_real_time () / 1000;
|
int64 now_millis = GLib.get_real_time () / 1000;
|
||||||
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions, (_, res) => {
|
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions, (_, res) => {
|
||||||
|
@ -240,7 +231,7 @@ public class Dino.Reactions : StreamInteractionModule, Object {
|
||||||
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
|
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
|
||||||
// Apply the same restrictions for incoming reactions as we do on sending them
|
// Apply the same restrictions for incoming reactions as we do on sending them
|
||||||
Conversation muc_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, account.bare_jid, account, MessageStanza.TYPE_GROUPCHAT);
|
Conversation muc_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, account.bare_jid, account, MessageStanza.TYPE_GROUPCHAT);
|
||||||
bool muc_supports_reactions = yield conversation_supports_reactions(muc_conversation);
|
bool muc_supports_reactions = conversation_supports_reactions(muc_conversation);
|
||||||
if (!muc_supports_reactions) return;
|
if (!muc_supports_reactions) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,6 @@ public class Dino.Replies : StreamInteractionModule, Object {
|
||||||
|
|
||||||
private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
// Check if a previous message was in reply to this one
|
// 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();
|
var reply_qry = db.reply.select();
|
||||||
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
|
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
|
||||||
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id);
|
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id);
|
||||||
|
|
|
@ -165,6 +165,7 @@ SOURCES
|
||||||
src/ui/conversation_content_view/file_default_widget.vala
|
src/ui/conversation_content_view/file_default_widget.vala
|
||||||
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/item_actions.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/quote_widget.vala
|
||||||
src/ui/conversation_content_view/reactions_widget.vala
|
src/ui/conversation_content_view/reactions_widget.vala
|
||||||
|
|
|
@ -203,23 +203,22 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
message_menu_box.visible = true;
|
message_menu_box.visible = true;
|
||||||
|
|
||||||
// Configure as many buttons as we need with the actions for the current meta item
|
// Configure as many buttons as we need with the actions for the current meta item
|
||||||
for (int i = 0; i < message_actions.size; i++) {
|
foreach (var message_action in message_actions) {
|
||||||
if (message_actions[i].popover != null) {
|
if (message_action.popover != null) {
|
||||||
MenuButton button = new MenuButton();
|
MenuButton button = new MenuButton();
|
||||||
button.icon_name = message_actions[i].icon_name;
|
button.sensitive = message_action.sensitive;
|
||||||
button.set_popover(message_actions[i].popover as Popover);
|
button.icon_name = message_action.icon_name;
|
||||||
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
|
button.set_popover(message_action.popover as Popover);
|
||||||
|
button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
|
||||||
action_buttons.add(button);
|
action_buttons.add(button);
|
||||||
}
|
} else if (message_action.callback != null) {
|
||||||
|
|
||||||
if (message_actions[i].callback != null) {
|
|
||||||
var message_action = message_actions[i];
|
|
||||||
Button button = new Button();
|
Button button = new Button();
|
||||||
|
button.sensitive = message_action.sensitive;
|
||||||
button.icon_name = message_action.icon_name;
|
button.icon_name = message_action.icon_name;
|
||||||
button.clicked.connect(() => {
|
button.clicked.connect(() => {
|
||||||
message_action.callback(button, current_meta_item, currently_highlighted);
|
message_action.callback(button, current_meta_item, currently_highlighted);
|
||||||
});
|
});
|
||||||
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
|
button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip);
|
||||||
action_buttons.add(button);
|
action_buttons.add(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,21 +32,8 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
|
||||||
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
|
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) {
|
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();
|
actions.add(get_reply_action(content_item, file_item.conversation, stream_interactor));
|
||||||
reply_action.icon_name = "mail-reply-sender-symbolic";
|
actions.add(get_reaction_action(content_item, file_item.conversation, stream_interactor));
|
||||||
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;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
50
main/src/ui/conversation_content_view/item_actions.vala
Normal file
50
main/src/ui/conversation_content_view/item_actions.vala
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
|
||||||
|
namespace Dino.Ui {
|
||||||
|
public Plugins.MessageAction get_reaction_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) {
|
||||||
|
Plugins.MessageAction action = new Plugins.MessageAction();
|
||||||
|
action.icon_name = "dino-emoticon-add-symbolic";
|
||||||
|
action.tooltip = _("Add reaction");
|
||||||
|
|
||||||
|
EmojiChooser chooser = new EmojiChooser();
|
||||||
|
chooser.emoji_picked.connect((emoji) => {
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(conversation, content_item, emoji);
|
||||||
|
});
|
||||||
|
action.popover = chooser;
|
||||||
|
|
||||||
|
// Disable the button if reaction aren't possible.
|
||||||
|
bool supports_reactions = stream_interactor.get_module(Reactions.IDENTITY).conversation_supports_reactions(conversation);
|
||||||
|
string? message_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
|
||||||
|
|
||||||
|
if (!supports_reactions) {
|
||||||
|
action.tooltip = _("This conversation does not support reactions.");
|
||||||
|
action.sensitive = false;
|
||||||
|
} else if (message_id == null) {
|
||||||
|
action.tooltip = "This message does not support reactions.";
|
||||||
|
action.sensitive = false;
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plugins.MessageAction get_reply_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) {
|
||||||
|
Plugins.MessageAction action = new Plugins.MessageAction();
|
||||||
|
action.icon_name = "mail-reply-sender-symbolic";
|
||||||
|
action.tooltip = _("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(conversation.id), new GLib.Variant.int32(content_item.id) }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disable the button if replies aren't possible.
|
||||||
|
string? message_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
|
||||||
|
if (message_id == null) {
|
||||||
|
action.sensitive = false;
|
||||||
|
if (conversation.type_.is_muc_semantic()) {
|
||||||
|
action.tooltip = _("This conversation does not support replies.");
|
||||||
|
} else {
|
||||||
|
action.tooltip = "This message does not support replies.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
|
|
||||||
MessageItemEditMode? edit_mode = null;
|
MessageItemEditMode? edit_mode = null;
|
||||||
ChatTextViewController? controller = null;
|
ChatTextViewController? controller = null;
|
||||||
private bool supports_reaction = false;
|
|
||||||
AdditionalInfo additional_info = AdditionalInfo.NONE;
|
AdditionalInfo additional_info = AdditionalInfo.NONE;
|
||||||
|
|
||||||
ulong realize_id = -1;
|
ulong realize_id = -1;
|
||||||
|
@ -36,8 +35,6 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
message_item = content_item as MessageItem;
|
message_item = content_item as MessageItem;
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
init.begin();
|
|
||||||
|
|
||||||
label.activate_link.connect(on_label_activate_link);
|
label.activate_link.connect(on_label_activate_link);
|
||||||
|
|
||||||
Message message = ((MessageItem) content_item).message;
|
Message message = ((MessageItem) content_item).message;
|
||||||
|
@ -71,10 +68,6 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
update_label();
|
update_label();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void init() {
|
|
||||||
supports_reaction = yield stream_interactor.get_module(Reactions.IDENTITY).conversation_supports_reactions(message_item.conversation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string generate_markup_text(ContentItem item) {
|
private string generate_markup_text(ContentItem item) {
|
||||||
MessageItem message_item = item as MessageItem;
|
MessageItem message_item = item as MessageItem;
|
||||||
Conversation conversation = message_item.conversation;
|
Conversation conversation = message_item.conversation;
|
||||||
|
@ -224,25 +217,9 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
actions.add(action1);
|
actions.add(action1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugins.MessageAction reply_action = new Plugins.MessageAction();
|
actions.add(get_reply_action(content_item, message_item.conversation, stream_interactor));
|
||||||
reply_action.icon_name = "mail-reply-sender-symbolic";
|
actions.add(get_reaction_action(content_item, message_item.conversation, stream_interactor));
|
||||||
reply_action.tooltip = _("Reply");
|
|
||||||
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(content_item.id) }));
|
|
||||||
};
|
|
||||||
actions.add(reply_action);
|
|
||||||
|
|
||||||
if (supports_reaction) {
|
|
||||||
Plugins.MessageAction action2 = new Plugins.MessageAction();
|
|
||||||
action2.icon_name = "dino-emoticon-add-symbolic";
|
|
||||||
action2.tooltip = _("Add reaction");
|
|
||||||
EmojiChooser chooser = new EmojiChooser();
|
|
||||||
chooser.emoji_picked.connect((emoji) => {
|
|
||||||
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji);
|
|
||||||
});
|
|
||||||
action2.popover = chooser;
|
|
||||||
actions.add(action2);
|
|
||||||
}
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue