Add support for last message correction

This commit is contained in:
fiaxh 2020-04-03 22:49:59 +02:00
parent 1c8e15c408
commit 871ff33ac7
36 changed files with 808 additions and 265 deletions

View file

@ -37,6 +37,7 @@ SOURCES
src/service/entity_info.vala src/service/entity_info.vala
src/service/file_manager.vala src/service/file_manager.vala
src/service/jingle_file_transfers.vala src/service/jingle_file_transfers.vala
src/service/message_correction.vala
src/service/message_processor.vala src/service/message_processor.vala
src/service/message_storage.vala src/service/message_storage.vala
src/service/module_manager.vala src/service/module_manager.vala

View file

@ -45,6 +45,7 @@ public interface Application : GLib.Application {
SearchProcessor.start(stream_interactor, db); SearchProcessor.start(stream_interactor, db);
Register.start(stream_interactor, db); Register.start(stream_interactor, db);
EntityInfo.start(stream_interactor, db); EntityInfo.start(stream_interactor, db);
MessageCorrection.start(stream_interactor, db);
create_actions(); create_actions();

View file

@ -57,6 +57,7 @@ public class Message : Object {
marked_ = value; marked_ = value;
} }
} }
public string? edit_to = null;
private Database? db; private Database? db;
@ -94,6 +95,8 @@ public class Message : Object {
string? real_jid_str = row[db.real_jid.real_jid]; string? real_jid_str = row[db.real_jid.real_jid];
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];
notify.connect(on_update); notify.connect(on_update);
} }

View file

@ -93,7 +93,6 @@ public abstract interface NotificationPopulator : Object {
public abstract class MetaConversationItem : Object { public abstract class MetaConversationItem : Object {
public virtual string populator_id { get; set; } public virtual string populator_id { get; set; }
public virtual Jid? jid { get; set; default=null; } public virtual Jid? jid { get; set; default=null; }
public virtual bool dim { get; set; default=false; }
public virtual DateTime sort_time { get; set; default = new DateTime.now_utc(); } public virtual DateTime sort_time { get; set; default = new DateTime.now_utc(); }
public virtual long seccondary_sort_indicator { get; set; } public virtual long seccondary_sort_indicator { get; set; }
public virtual long tertiary_sort_indicator { get; set; } public virtual long tertiary_sort_indicator { get; set; }
@ -101,11 +100,19 @@ public abstract class MetaConversationItem : Object {
public virtual Encryption encryption { get; set; default = Encryption.NONE; } public virtual Encryption encryption { get; set; default = Encryption.NONE; }
public virtual Entities.Message.Marked mark { get; set; default = Entities.Message.Marked.NONE; } public virtual Entities.Message.Marked mark { get; set; default = Entities.Message.Marked.NONE; }
public abstract bool can_merge { get; set; } public bool can_merge { get; set; default=false; }
public abstract bool requires_avatar { get; set; } public bool requires_avatar { get; set; default=false; }
public abstract bool requires_header { get; set; } public bool requires_header { get; set; default=false; }
public bool in_edit_mode { get; set; default=false; }
public abstract Object? get_widget(WidgetType type); public abstract Object? get_widget(WidgetType type);
public abstract Gee.List<MessageAction>? get_item_actions(WidgetType type);
}
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
public class MessageAction : Object {
public string icon_name;
public MessageActionEvoked callback;
} }
public abstract class MetaConversationNotification : Object { public abstract class MetaConversationNotification : Object {

View file

@ -45,9 +45,13 @@ public class ContentItemStore : StreamInteractionModule, Object {
foreach (var row in select) { foreach (var row in select) {
int provider = row[db.content_item.content_type]; int provider = row[db.content_item.content_type];
int foreign_id = row[db.content_item.foreign_id]; int foreign_id = row[db.content_item.foreign_id];
DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]);
DateTime local_time = new DateTime.from_unix_utc(row[db.content_item.local_time]);
switch (provider) { switch (provider) {
case 1: case 1:
RowOption row_option = db.message.select().with(db.message.id, "=", foreign_id).row(); RowOption row_option = db.message.select().with(db.message.id, "=", foreign_id)
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.row();
if (row_option.is_present()) { if (row_option.is_present()) {
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);
if (message == null) { if (message == null) {
@ -58,7 +62,10 @@ public class ContentItemStore : StreamInteractionModule, Object {
} }
} }
if (message != null) { if (message != null) {
items.add(new MessageItem(message, conversation, row[db.content_item.id])); var message_item = new MessageItem(message, conversation, row[db.content_item.id]);
message_item.display_time = time;
message_item.sort_time = local_time;
items.add(message_item);
} }
} }
break; break;
@ -259,6 +266,7 @@ public class MessageItem : ContentItem {
public MessageItem(Message message, Conversation conversation, int id) { public MessageItem(Message message, Conversation conversation, int id) {
base(id, TYPE, message.from, message.local_time, message.time, message.encryption, message.marked); base(id, TYPE, message.from, message.local_time, message.time, message.encryption, message.marked);
this.message = message; this.message = message;
this.conversation = conversation; this.conversation = conversation;

View file

@ -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 = 13; private const int VERSION = 14;
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,18 @@ public class Database : Qlite.Database {
} }
} }
public class MessageCorrectionTable : 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") { unique=true };
public Column<string> to_stanza_id = new Column.Text("to_stanza_id");
internal MessageCorrectionTable(Database db) {
base(db, "message_correction");
init({id, message_id, to_stanza_id});
index("message_correction_to_stanza_id_idx", {to_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");
@ -247,6 +259,7 @@ 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 MessageCorrectionTable message_correction { get; private set; }
public RealJidTable real_jid { get; private set; } public RealJidTable real_jid { get; private set; }
public FileTransferTable file_transfer { get; private set; } public FileTransferTable file_transfer { get; private set; }
public ConversationTable conversation { get; private set; } public ConversationTable conversation { get; private set; }
@ -268,6 +281,7 @@ 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);
message_correction = new MessageCorrectionTable(this);
real_jid = new RealJidTable(this); real_jid = new RealJidTable(this);
file_transfer = new FileTransferTable(this); file_transfer = new FileTransferTable(this);
conversation = new ConversationTable(this); conversation = new ConversationTable(this);
@ -277,7 +291,7 @@ public class Database : Qlite.Database {
roster = new RosterTable(this); roster = new RosterTable(this);
mam_catchup = new MamCatchupTable(this); mam_catchup = new MamCatchupTable(this);
settings = new SettingsTable(this); settings = new SettingsTable(this);
init({ account, jid, entity, content_item, message, real_jid, file_transfer, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings }); init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings });
try { try {
exec("PRAGMA synchronous=0"); exec("PRAGMA synchronous=0");
} catch (Error e) { } } catch (Error e) { }
@ -401,14 +415,14 @@ public class Database : Qlite.Database {
if (before != null) { if (before != null) {
if (id > 0) { if (id > 0) {
select.where(@"local_time < ? OR (local_time = ? AND id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() }); select.where(@"local_time < ? OR (local_time = ? AND message.id < ?)", { before.to_unix().to_string(), before.to_unix().to_string(), id.to_string() });
} else { } else {
select.with(message.id, "<", id); select.with(message.id, "<", id);
} }
} }
if (after != null) { if (after != null) {
if (id > 0) { if (id > 0) {
select.where(@"local_time > ? OR (local_time = ? AND id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() }); select.where(@"local_time > ? OR (local_time = ? AND message.id > ?)", { after.to_unix().to_string(), after.to_unix().to_string(), id.to_string() });
} else { } else {
select.with(message.local_time, ">", (long) after.to_unix()); select.with(message.local_time, ">", (long) after.to_unix());
} }
@ -430,6 +444,7 @@ public class Database : Qlite.Database {
} }
select.outer_join_with(real_jid, real_jid.message_id, message.id); select.outer_join_with(real_jid, real_jid.message_id, message.id);
select.outer_join_with(message_correction, message_correction.message_id, message.id);
LinkedList<Message> ret = new LinkedList<Message>(); LinkedList<Message> ret = new LinkedList<Message>();
foreach (Row row in select) { foreach (Row row in select) {

View file

@ -0,0 +1,175 @@
using Gee;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
using Qlite;
namespace Dino {
public class MessageCorrection : StreamInteractionModule, MessageListener {
public static ModuleIdentity<MessageCorrection> IDENTITY = new ModuleIdentity<MessageCorrection>("message_correction");
public string id { get { return IDENTITY.id; } }
public signal void received_correction(ContentItem content_item);
private StreamInteractor stream_interactor;
private Database db;
private HashMap<Conversation, HashMap<Jid, Message>> last_messages = new HashMap<Conversation, HashMap<Jid, Message>>(Conversation.hash_func, Conversation.equals_func);
private HashMap<string, string> outstanding_correction_nodes = new HashMap<string, string>();
public static void start(StreamInteractor stream_interactor, Database db) {
MessageCorrection m = new MessageCorrection(stream_interactor, db);
stream_interactor.add_module(m);
}
public MessageCorrection(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(this);
stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_correction_node);
stream_interactor.get_module(PresenceManager.IDENTITY).received_offline_presence.connect((jid, account) => {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid.bare_jid, account, Conversation.Type.GROUPCHAT);
if (conversation != null) {
if (last_messages.has_key(conversation)) last_messages[conversation].unset(jid);
}
});
}
public void send_correction(Conversation conversation, Message old_message, string correction_text) {
string stanza_id = old_message.edit_to ?? old_message.stanza_id;
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation);
out_message.edit_to = stanza_id;
outstanding_correction_nodes[out_message.stanza_id] = stanza_id;
stream_interactor.get_module(MessageStorage.IDENTITY).add_message(out_message, conversation);
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation);
db.message_correction.insert()
.value(db.message_correction.message_id, out_message.id)
.value(db.message_correction.to_stanza_id, stanza_id)
.perform();
db.content_item.update()
.with(db.content_item.foreign_id, "=", old_message.id)
.with(db.content_item.content_type, "=", 1)
.set(db.content_item.foreign_id, out_message.id)
.perform();
on_received_correction(conversation, out_message.id);
}
public bool is_own_correction_allowed(Conversation conversation, Message message) {
string stanza_id = message.edit_to ?? message.stanza_id;
Jid own_jid = conversation.account.full_jid;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
own_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account);
}
return last_messages.has_key(conversation) &&
last_messages[conversation].has_key(own_jid) &&
last_messages[conversation][own_jid].stanza_id == stanza_id;
}
private void check_add_correction_node(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
if (message.stanza_id in outstanding_correction_nodes) {
LastMessageCorrection.set_replace_id(message_stanza, outstanding_correction_nodes[message.stanza_id]);
outstanding_correction_nodes.unset(message.stanza_id);
} else {
if (!last_messages.has_key(conversation)) {
last_messages[conversation] = new HashMap<Jid, Message>(Jid.hash_func, Jid.equals_func);
}
last_messages[conversation][message.from] = message;
}
}
public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT", "FILTER_EMPTY" };
public override string action_group { get { return "CORRECTION"; } }
public override string[] after_actions { get { return after_actions_const; } }
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
string? replace_id = Xep.LastMessageCorrection.get_replace_id(stanza);
if (replace_id == null) {
if (!last_messages.has_key(conversation)) {
last_messages[conversation] = new HashMap<Jid, Message>(Jid.hash_func, Jid.equals_func);
}
last_messages[conversation][message.from] = message;
return false;
}
if (!last_messages.has_key(conversation) || !last_messages[conversation].has_key(message.from)) return false;
Message original_message = last_messages[conversation][message.from];
if (original_message.stanza_id != replace_id) return false;
int message_id_to_be_updated = get_latest_correction_message_id(conversation.account.id, replace_id, db.get_jid_id(message.counterpart), message.counterpart.resourcepart);
if (message_id_to_be_updated == -1) {
message_id_to_be_updated = original_message.id;
}
db.message_correction.insert()
.value(db.message_correction.message_id, message.id)
.value(db.message_correction.to_stanza_id, replace_id)
.perform();
int current_correction_message_id = get_latest_correction_message_id(conversation.account.id, replace_id, db.get_jid_id(message.counterpart), message.counterpart.resourcepart);
if (current_correction_message_id != message_id_to_be_updated) {
db.content_item.update()
.with(db.content_item.foreign_id, "=", message_id_to_be_updated)
.with(db.content_item.content_type, "=", 1)
.set(db.content_item.foreign_id, current_correction_message_id)
.perform();
message.edit_to = replace_id;
on_received_correction(conversation, current_correction_message_id);
}
return true;
}
private void on_received_correction(Conversation conversation, int message_id) {
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message_id);
received_correction(content_item);
}
private int get_latest_correction_message_id(int account_id, string stanza_id, int counterpart_jid_id, string? counterpart_resource) {
var qry = db.message_correction.select({db.message.id})
.join_with(db.message, db.message.id, db.message_correction.message_id)
.with(db.message.account_id, "=", account_id)
.with(db.message.counterpart_id, "=", counterpart_jid_id)
.with(db.message_correction.to_stanza_id, "=", stanza_id)
.order_by(db.message.time, "DESC");
if (counterpart_resource != null) {
qry.with(db.message.counterpart_resource, "=", counterpart_resource);
}
RowOption row = qry.single().row();
if (row.is_present()) {
return row[db.message.id];
}
return -1;
}
private void on_account_added(Account account) {
Gee.List<Conversation> conversations = stream_interactor.get_module(ConversationManager.IDENTITY).get_active_conversations(account);
foreach (Conversation conversation in conversations) {
if (conversation.type_ != Conversation.Type.CHAT) continue;
HashMap<Jid, Message> last_conversation_messages = new HashMap<Jid, Message>(Jid.hash_func, Jid.equals_func);
Gee.List<Message> messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation);
for (int i = messages.size - 1; i > 0; i--) {
Message message = messages[i];
if (!last_conversation_messages.has_key(message.from) && message.edit_to == null) {
last_conversation_messages[message.from] = message;
}
}
last_messages[conversation] = last_conversation_messages;
}
}
}
}

View file

@ -40,6 +40,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
received_pipeline.connect(new DeduplicateMessageListener(this, db)); received_pipeline.connect(new DeduplicateMessageListener(this, db));
received_pipeline.connect(new FilterMessageListener()); received_pipeline.connect(new FilterMessageListener());
received_pipeline.connect(new StoreMessageListener(stream_interactor)); received_pipeline.connect(new StoreMessageListener(stream_interactor));
received_pipeline.connect(new StoreContentItemListener(stream_interactor));
received_pipeline.connect(new MamMessageListener(stream_interactor)); received_pipeline.connect(new MamMessageListener(stream_interactor));
stream_interactor.account_added.connect(on_account_added); stream_interactor.account_added.connect(on_account_added);
@ -62,6 +63,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
public Entities.Message send_message(Entities.Message message, Conversation conversation) { public Entities.Message send_message(Entities.Message message, Conversation conversation) {
stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation); stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation);
stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation);
send_xmpp_message(message, conversation); send_xmpp_message(message, conversation);
message_sent(message, conversation); message_sent(message, conversation);
return message; return message;
@ -526,6 +528,25 @@ public class MessageProcessor : StreamInteractionModule, Object {
} }
} }
private class StoreContentItemListener : MessageListener {
public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT", "FILTER_EMPTY", "STORE", "CORRECTION" };
public override string action_group { get { return "STORE_CONTENT_ITEM"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
public StoreContentItemListener(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
if (message.body == null) return true;
stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation);
return false;
}
}
private class MamMessageListener : MessageListener { private class MamMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "DEDUPLICATE" }; public string[] after_actions_const = new string[]{ "DEDUPLICATE" };

View file

@ -28,7 +28,6 @@ public class MessageStorage : StreamInteractionModule, Object {
message.persist(db); message.persist(db);
init_conversation(conversation); init_conversation(conversation);
messages[conversation].add(message); messages[conversation].add(message);
stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation);
} }
public Gee.List<Message> get_messages(Conversation conversation, int count = 50) { public Gee.List<Message> get_messages(Conversation conversation, int count = 50) {

View file

@ -79,6 +79,7 @@ public class ModuleManager {
module_map[account].add(new Xep.JingleInBandBytestreams.Module()); module_map[account].add(new Xep.JingleInBandBytestreams.Module());
module_map[account].add(new Xep.JingleFileTransfer.Module()); module_map[account].add(new Xep.JingleFileTransfer.Module());
module_map[account].add(new Xep.Jet.Module()); module_map[account].add(new Xep.Jet.Module());
module_map[account].add(new Xep.LastMessageCorrection.Module());
initialize_account_modules(account, module_map[account]); initialize_account_modules(account, module_map[account]);
} }
} }

View file

@ -62,6 +62,7 @@ set(RESOURCE_LIST
menu_app.ui menu_app.ui
menu_conversation.ui menu_conversation.ui
menu_encryption.ui menu_encryption.ui
message_item_widget_edit_mode.ui
occupant_list.ui occupant_list.ui
occupant_list_item.ui occupant_list_item.ui
search_autocomplete.ui search_autocomplete.ui
@ -119,6 +120,7 @@ SOURCES
src/ui/add_conversation/select_jid_fragment.vala src/ui/add_conversation/select_jid_fragment.vala
src/ui/chat_input/chat_input_controller.vala src/ui/chat_input/chat_input_controller.vala
src/ui/chat_input/chat_text_view.vala
src/ui/chat_input/edit_history.vala src/ui/chat_input/edit_history.vala
src/ui/chat_input/encryption_button.vala src/ui/chat_input/encryption_button.vala
src/ui/chat_input/occupants_tab_completer.vala src/ui/chat_input/occupants_tab_completer.vala
@ -134,12 +136,12 @@ SOURCES
src/ui/conversation_selector/conversation_selector.vala src/ui/conversation_selector/conversation_selector.vala
src/ui/conversation_content_view/chat_state_populator.vala src/ui/conversation_content_view/chat_state_populator.vala
src/ui/conversation_content_view/content_item_widget_factory.vala
src/ui/conversation_content_view/content_populator.vala src/ui/conversation_content_view/content_populator.vala
src/ui/conversation_content_view/conversation_item_skeleton.vala src/ui/conversation_content_view/conversation_item_skeleton.vala
src/ui/conversation_content_view/conversation_view.vala src/ui/conversation_content_view/conversation_view.vala
src/ui/conversation_content_view/date_separator_populator.vala src/ui/conversation_content_view/date_separator_populator.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/subscription_notification.vala src/ui/conversation_content_view/subscription_notification.vala
src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/menu_entry.vala

View file

@ -45,20 +45,8 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="scrolled"> <object class="DinoUiChatTextView" id="chat_text_view">
<property name="max_content_height">300</property>
<property name="propagate_natural_height">true</property>
<property name="visible">True</property> <property name="visible">True</property>
<child>
<object class="GtkTextView" id="text_input">
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="margin">8</property>
<property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
<property name="valign">center</property>
<property name="visible">True</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>

View file

@ -46,14 +46,12 @@
<property name="halign">end</property> <property name="halign">end</property>
<property name="valign">start</property> <property name="valign">start</property>
<child> <child>
<object class="GtkMenuButton" id="emoji_button"> <object class="GtkButton" id="button1">
<property name="vexpand">False</property> <property name="vexpand">False</property>
<property name="halign">end</property> <property name="halign">end</property>
<property name="valign">end</property> <property name="valign">end</property>
<property name="visible">True</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage" id="button1_icon">
<property name="icon-name">dino-emoticon-add-symbolic</property>
<property name="icon-size">1</property> <property name="icon-size">1</property>
<property name="visible">True</property> <property name="visible">True</property>
</object> </object>

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.22"/>
<template class="DinoUiConversationSummaryMessageItemEditMode" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="visible">True</property>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<style>
<class name="dino-chatinput"/>
</style>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<child>
<object class="DinoUiChatTextView" id="chat_text_view">
<property name="visible">True</property>
</object>
</child>
<child>
<object class="GtkMenuButton" id="emoji_button">
<property name="relief">none</property>
<property name="margin-top">3</property>
<property name="valign">start</property>
<property name="visible">True</property>
<style>
<class name="dino-chatinput-button"/>
<class name="flat"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">dino-emoticon-symbolic</property>
<property name="icon-size">1</property>
<property name="visible">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">5</property>
<property name="halign">end</property>
<property name="visible">True</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">Cancel</property>
<property name="visible">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="label">Update message</property>
<property name="visible">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -52,6 +52,14 @@ window.dino-main .dino-sidebar > frame {
transition: background .05s ease; transition: background .05s ease;
} }
window.dino-main .dino-conversation .message-box.edit-mode {
background: alpha(@theme_selected_bg_color, 0.1);
}
window.dino-main .dino-conversation .message-box.edit-mode:hover {
background: alpha(@theme_selected_bg_color, 0.12);
}
window.dino-main .dino-conversation .file-box-outer { window.dino-main .dino-conversation .file-box-outer {
background: @theme_base_color; background: @theme_base_color;
border-radius: 3px; border-radius: 3px;
@ -110,7 +118,9 @@ window.dino-main button.dino-chatinput-button:checked:backdrop {
} }
.dino-chatinput textview, .dino-chatinput textview text { .dino-chatinput,
.dino-chatinput textview,
.dino-chatinput textview text {
background-color: @theme_base_color; background-color: @theme_base_color;
} }

View file

@ -17,18 +17,20 @@ public class ChatInputController : Object {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Plugins.InputFieldStatus input_field_status; private Plugins.InputFieldStatus input_field_status;
private ChatTextViewController chat_text_view_controller;
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;
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.chat_text_view_controller = new ChatTextViewController(chat_input.chat_text_view, stream_interactor);
chat_input.init(stream_interactor); chat_input.init(stream_interactor);
reset_input_field_status(); reset_input_field_status();
chat_input.text_input.buffer.changed.connect(on_text_input_changed); chat_input.chat_text_view.text_view.buffer.changed.connect(on_text_input_changed);
chat_input.send_text.connect(send_text); chat_text_view_controller.send_text.connect(send_text);
chat_input.encryption_widget.encryption_changed.connect(on_encryption_changed); chat_input.encryption_widget.encryption_changed.connect(on_encryption_changed);
@ -40,10 +42,10 @@ public class ChatInputController : Object {
reset_input_field_status(); reset_input_field_status();
chat_input.initialize_for_conversation(conversation);
chat_input.occupants_tab_completor.initialize_for_conversation(conversation);
chat_input.edit_history.initialize_for_conversation(conversation);
chat_input.encryption_widget.set_conversation(conversation); chat_input.encryption_widget.set_conversation(conversation);
chat_input.initialize_for_conversation(conversation);
chat_text_view_controller.initialize_for_conversation(conversation);
} }
private void on_encryption_changed(Plugins.EncryptionListEntry? encryption_entry) { private void on_encryption_changed(Plugins.EncryptionListEntry? encryption_entry) {
@ -81,8 +83,8 @@ public class ChatInputController : Object {
return; return;
} }
string text = chat_input.text_input.buffer.text; string text = chat_input.chat_text_view.text_view.buffer.text;
chat_input.text_input.buffer.text = ""; chat_input.chat_text_view.text_view.buffer.text = "";
if (text.has_prefix("/")) { if (text.has_prefix("/")) {
string[] token = text.split(" ", 2); string[] token = text.split(" ", 2);
switch(token[0]) { switch(token[0]) {
@ -137,7 +139,7 @@ public class ChatInputController : Object {
} }
private void on_text_input_changed() { private void on_text_input_changed() {
if (chat_input.text_input.buffer.text != "") { if (chat_input.chat_text_view.text_view.buffer.text != "") {
stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_entered(conversation); stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_entered(conversation);
} else { } else {
stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_cleared(conversation); stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_cleared(conversation);

View file

@ -0,0 +1,91 @@
using Gdk;
using Gee;
using Gtk;
using Dino.Entities;
using Xmpp;
namespace Dino.Ui {
public class ChatTextViewController : Object {
public signal void send_text();
public OccupantsTabCompletor occupants_tab_completor;
private ChatTextView widget;
public ChatTextViewController(ChatTextView widget, StreamInteractor stream_interactor) {
this.widget = widget;
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, widget.text_view);
widget.send_text.connect(() => {
send_text();
});
}
public void initialize_for_conversation(Conversation conversation) {
occupants_tab_completor.initialize_for_conversation(conversation);
widget.initialize_for_conversation(conversation);
}
}
public class ChatTextView : ScrolledWindow {
public signal void send_text();
public signal void cancel_input();
public TextView text_view = new TextView() { can_focus=true, hexpand=true, margin=8, wrap_mode=Gtk.WrapMode.WORD_CHAR, valign=Align.CENTER, visible=true };
private int vscrollbar_min_height;
private SmileyConverter smiley_converter;
public EditHistory edit_history;
construct {
max_content_height = 300;
propagate_natural_height = true;
this.add(text_view);
smiley_converter = new SmileyConverter(text_view);
edit_history = new EditHistory(text_view);
this.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null);
this.vadjustment.notify["upper"].connect_after(on_upper_notify);
text_view.key_press_event.connect(on_text_input_key_press);
Gtk.drag_dest_unset(text_view);
}
public void initialize_for_conversation(Conversation conversation) {
edit_history.initialize_for_conversation(conversation);
}
public override void get_preferred_height(out int min_height, out int nat_height) {
base.get_preferred_height(out min_height, out nat_height);
min_height = nat_height;
}
private void on_upper_notify() {
this.vadjustment.value = this.vadjustment.upper - this.vadjustment.page_size;
// hack for vscrollbar not requiring space and making textview higher //TODO doesn't resize immediately
this.get_vscrollbar().visible = (this.vadjustment.upper > this.max_content_height - 2 * this.vscrollbar_min_height);
}
private bool on_text_input_key_press(EventKey event) {
if (event.keyval in new uint[]{Key.Return, Key.KP_Enter}) {
if ((event.state & ModifierType.SHIFT_MASK) > 0) {
text_view.buffer.insert_at_cursor("\n", 1);
} else if (text_view.buffer.text != "") {
send_text();
edit_history.reset_history();
}
return true;
}
if (event.keyval == Key.Escape) {
cancel_input();
}
return false;
}
}
}

View file

@ -4,7 +4,7 @@ using Gtk;
using Dino.Entities; using Dino.Entities;
namespace Dino.Ui.ChatInput { namespace Dino.Ui {
public class EditHistory { public class EditHistory {
@ -14,7 +14,7 @@ public class EditHistory {
private HashMap<Conversation, Gee.List<string>> histories = new HashMap<Conversation, Gee.List<string>>(Conversation.hash_func, Conversation.equals_func); private HashMap<Conversation, Gee.List<string>> histories = new HashMap<Conversation, Gee.List<string>>(Conversation.hash_func, Conversation.equals_func);
private HashMap<Conversation, int> indices = new HashMap<Conversation, int>(Conversation.hash_func, Conversation.equals_func); private HashMap<Conversation, int> indices = new HashMap<Conversation, int>(Conversation.hash_func, Conversation.equals_func);
public EditHistory(TextView text_input, GLib.Application application) { public EditHistory(TextView text_input) {
this.text_input = text_input; this.text_input = text_input;
text_input.key_press_event.connect(on_text_input_key_press); text_input.key_press_event.connect(on_text_input_key_press);

View file

@ -5,7 +5,7 @@ using Gtk;
using Dino.Entities; using Dino.Entities;
using Xmpp; using Xmpp;
namespace Dino.Ui.ChatInput { namespace Dino.Ui {
/** /**
* - With given prefix: Complete from occupant list (sorted lexicographically) * - With given prefix: Complete from occupant list (sorted lexicographically)

View file

@ -4,7 +4,7 @@ using Gtk;
using Dino.Entities; using Dino.Entities;
namespace Dino.Ui.ChatInput { namespace Dino.Ui {
class SmileyConverter { class SmileyConverter {

View file

@ -10,25 +10,17 @@ namespace Dino.Ui.ChatInput {
[GtkTemplate (ui = "/im/dino/Dino/chat_input.ui")] [GtkTemplate (ui = "/im/dino/Dino/chat_input.ui")]
public class View : Box { public class View : Box {
public signal void send_text();
public string text { public string text {
owned get { return text_input.buffer.text; } owned get { return chat_text_view.text_view.buffer.text; }
set { text_input.buffer.text = value; } set { chat_text_view.text_view.buffer.text = value; }
} }
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Conversation? conversation; private Conversation? conversation;
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);
private int vscrollbar_min_height;
public OccupantsTabCompletor occupants_tab_completor;
private SmileyConverter smiley_converter;
public EditHistory edit_history;
[GtkChild] public Frame frame; [GtkChild] public Frame frame;
[GtkChild] public ScrolledWindow scrolled; [GtkChild] public ChatTextView chat_text_view;
[GtkChild] public TextView text_input;
[GtkChild] public Box outer_box; [GtkChild] public Box outer_box;
[GtkChild] public Button file_button; [GtkChild] public Button file_button;
[GtkChild] public Separator file_separator; [GtkChild] public Separator file_separator;
@ -39,9 +31,6 @@ public class View : Box {
public View init(StreamInteractor stream_interactor) { public View init(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input);
smiley_converter = new SmileyConverter(text_input);
edit_history = new EditHistory(text_input, GLib.Application.get_default());
encryption_widget = new EncryptionButton(stream_interactor) { relief=ReliefStyle.NONE, margin_top=3, valign=Align.START, visible=true }; encryption_widget = new EncryptionButton(stream_interactor) { relief=ReliefStyle.NONE, margin_top=3, valign=Align.START, visible=true };
file_button.clicked.connect(() => { file_button.clicked.connect(() => {
@ -53,9 +42,6 @@ public class View : Box {
}); });
file_button.get_style_context().add_class("dino-attach-button"); file_button.get_style_context().add_class("dino-attach-button");
scrolled.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null);
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
encryption_widget.get_style_context().add_class("dino-chatinput-button"); encryption_widget.get_style_context().add_class("dino-chatinput-button");
encryption_widget.encryption_changed.connect(update_file_transfer_availability); encryption_widget.encryption_changed.connect(update_file_transfer_availability);
@ -68,7 +54,7 @@ public class View : Box {
EmojiChooser chooser = new EmojiChooser(); EmojiChooser chooser = new EmojiChooser();
chooser.emoji_picked.connect((emoji) => { chooser.emoji_picked.connect((emoji) => {
text_input.buffer.insert_at_cursor(emoji, emoji.data.length); chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length);
}); });
emoji_button.set_popover(chooser); emoji_button.set_popover(chooser);
@ -77,8 +63,6 @@ public class View : Box {
outer_box.add(encryption_widget); outer_box.add(encryption_widget);
text_input.key_press_event.connect(on_text_input_key_press);
Util.force_css(frame, "* { border-radius: 3px; }"); Util.force_css(frame, "* { border-radius: 3px; }");
return this; return this;
@ -91,17 +75,17 @@ public class View : Box {
} }
public void initialize_for_conversation(Conversation conversation) { public void initialize_for_conversation(Conversation conversation) {
if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text; if (this.conversation != null) entry_cache[this.conversation] = chat_text_view.text_view.buffer.text;
this.conversation = conversation; this.conversation = conversation;
update_file_transfer_availability(); update_file_transfer_availability();
text_input.buffer.text = ""; chat_text_view.text_view.buffer.text = "";
if (entry_cache.has_key(conversation)) { if (entry_cache.has_key(conversation)) {
text_input.buffer.text = entry_cache[conversation]; chat_text_view.text_view.buffer.text = entry_cache[conversation];
} }
text_input.grab_focus(); chat_text_view.text_view.grab_focus();
} }
public void set_input_state(Plugins.InputFieldStatus.MessageType message_type) { public void set_input_state(Plugins.InputFieldStatus.MessageType message_type) {
@ -132,26 +116,6 @@ public class View : Box {
return false; return false;
}); });
} }
private bool on_text_input_key_press(EventKey event) {
if (event.keyval in new uint[]{Key.Return, Key.KP_Enter}) {
if ((event.state & ModifierType.SHIFT_MASK) > 0) {
text_input.buffer.insert_at_cursor("\n", 1);
} else if (this.text != "") {
send_text();
edit_history.reset_history();
}
return true;
}
return false;
}
private void on_upper_notify() {
scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size;
// hack for vscrollbar not requiring space and making textview higher //TODO doesn't resize immediately
scrolled.get_vscrollbar().visible = (scrolled.vadjustment.upper > scrolled.max_content_height - 2 * vscrollbar_min_height);
}
} }
} }

View file

@ -64,10 +64,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.Conversati
private class MetaChatStateItem : Plugins.MetaConversationItem { private class MetaChatStateItem : Plugins.MetaConversationItem {
public override DateTime sort_time { get; set; default=new DateTime.now_utc().add_years(10); } public override DateTime sort_time { get; set; default=new DateTime.now_utc().add_years(10); }
public override bool can_merge { get; set; default=false; }
public override bool requires_avatar { get; set; default=false; }
public override bool requires_header { get; set; default=false; }
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Conversation conversation; private Conversation conversation;
private Gee.List<Jid> jids = new ArrayList<Jid>(); private Gee.List<Jid> jids = new ArrayList<Jid>();
@ -93,6 +89,8 @@ private class MetaChatStateItem : Plugins.MetaConversationItem {
return image_content_box; return image_content_box;
} }
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
public void set_new(Gee.List<Jid> jids) { public void set_new(Gee.List<Jid> jids) {
this.jids = jids; this.jids = jids;
update(); update();

View file

@ -1,114 +0,0 @@
using Gee;
using Gdk;
using Gtk;
using Pango;
using Xmpp;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class ContentItemWidgetFactory : Object {
private StreamInteractor stream_interactor;
private HashMap<string, WidgetGenerator> generators = new HashMap<string, WidgetGenerator>();
public ContentItemWidgetFactory(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor);
generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor);
}
public Widget? get_widget(ContentItem item) {
WidgetGenerator? generator = generators[item.type_];
if (generator != null) {
return (Widget?) generator.get_widget(item);
}
return null;
}
public void register_widget_generator(WidgetGenerator generator) {
generators[generator.handles_type] = generator;
}
}
public interface WidgetGenerator : Object {
public abstract string handles_type { get; set; }
public abstract Object get_widget(ContentItem item);
}
public class MessageItemWidgetGenerator : WidgetGenerator, Object {
public string handles_type { get; set; default=MessageItem.TYPE; }
private StreamInteractor stream_interactor;
public MessageItemWidgetGenerator(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public Object get_widget(ContentItem item) {
MessageItem message_item = item as MessageItem;
Conversation conversation = message_item.conversation;
Message message = message_item.message;
Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true };
string markup_text = message.body;
if (markup_text.length > 10000) {
markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]";
}
if (message_item.message.body.has_prefix("/me")) {
markup_text = markup_text.substring(3);
}
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true);
} else {
markup_text = Util.parse_add_markup(markup_text, null, true, true);
}
if (message_item.message.body.has_prefix("/me")) {
string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from);
update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text);
label.realize.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text));
label.style_updated.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text));
}
int only_emoji_count = Util.get_only_emoji_count(markup_text);
if (only_emoji_count != -1) {
string size_str = only_emoji_count < 5 ? "xx-large" : "large";
markup_text = @"<span size=\'$size_str\'>" + markup_text + "</span>";
}
label.label = markup_text;
return label;
}
public static void update_me_style(StreamInteractor stream_interactor, Jid jid, string display_name, Account account, Label label, string action_text) {
string color = Util.get_name_hex_color(stream_interactor, account, jid, Util.is_dark_theme(label));
label.label = @"<span color=\"#$(color)\">$(Markup.escape_text(display_name))</span>" + action_text;
}
}
public class FileItemWidgetGenerator : WidgetGenerator, Object {
public StreamInteractor stream_interactor;
public string handles_type { get; set; default=FileItem.TYPE; }
private const int MAX_HEIGHT = 300;
private const int MAX_WIDTH = 600;
public FileItemWidgetGenerator(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public Object get_widget(ContentItem item) {
FileItem file_item = item as FileItem;
FileTransfer transfer = file_item.file_transfer;
return new FileWidget(stream_interactor, transfer) { visible=true };
}
}
}

View file

@ -9,13 +9,11 @@ namespace Dino.Ui.ConversationSummary {
public class ContentProvider : ContentItemCollection, Object { public class ContentProvider : ContentItemCollection, Object {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private ContentItemWidgetFactory widget_factory;
private Conversation? current_conversation; private Conversation? current_conversation;
private Plugins.ConversationItemCollection? item_collection; private Plugins.ConversationItemCollection? item_collection;
public ContentProvider(StreamInteractor stream_interactor) { public ContentProvider(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.widget_factory = new ContentItemWidgetFactory(stream_interactor);
} }
public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) { public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) {
@ -28,7 +26,7 @@ public class ContentProvider : ContentItemCollection, Object {
} }
public void insert_item(ContentItem item) { public void insert_item(ContentItem item) {
item_collection.insert_item(new ContentMetaItem(item, widget_factory)); item_collection.insert_item(create_content_meta_item(item));
} }
public void remove_item(ContentItem item) { } public void remove_item(ContentItem item) { }
@ -38,7 +36,7 @@ public class ContentProvider : ContentItemCollection, Object {
Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_n_latest(conversation, n); Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_n_latest(conversation, n);
Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
foreach (ContentItem item in items) { foreach (ContentItem item in items) {
ret.add(new ContentMetaItem(item, widget_factory)); ret.add(create_content_meta_item(item));
} }
return ret; return ret;
} }
@ -47,7 +45,7 @@ public class ContentProvider : ContentItemCollection, Object {
Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n); Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n);
foreach (ContentItem item in items) { foreach (ContentItem item in items) {
ret.add(new ContentMetaItem(item, widget_factory)); ret.add(create_content_meta_item(item));
} }
return ret; return ret;
} }
@ -56,26 +54,30 @@ public class ContentProvider : ContentItemCollection, Object {
Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n); Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n);
foreach (ContentItem item in items) { foreach (ContentItem item in items) {
ret.add(new ContentMetaItem(item, widget_factory)); ret.add(create_content_meta_item(item));
} }
return ret; return ret;
} }
public ContentMetaItem get_content_meta_item(ContentItem content_item) { public ContentMetaItem get_content_meta_item(ContentItem content_item) {
return new ContentMetaItem(content_item, widget_factory); return create_content_meta_item(content_item);
}
private ContentMetaItem create_content_meta_item(ContentItem content_item) {
if (content_item.type_ == MessageItem.TYPE) {
return new MessageMetaItem(content_item, stream_interactor);
} else if (content_item.type_ == FileItem.TYPE) {
return new FileMetaItem(content_item, stream_interactor);
}
return null;
} }
} }
public class ContentMetaItem : Plugins.MetaConversationItem { public abstract class ContentMetaItem : Plugins.MetaConversationItem {
public override Jid? jid { get; set; }
public override DateTime sort_time { get; set; }
public override DateTime? display_time { get; set; }
public override Encryption encryption { get; set; }
public ContentItem content_item; public ContentItem content_item;
private ContentItemWidgetFactory widget_factory;
public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { protected ContentMetaItem(ContentItem content_item) {
this.jid = content_item.jid; this.jid = content_item.jid;
this.sort_time = content_item.sort_time; this.sort_time = content_item.sort_time;
this.seccondary_sort_indicator = (long) content_item.display_time.to_unix(); this.seccondary_sort_indicator = (long) content_item.display_time.to_unix();
@ -96,15 +98,6 @@ public class ContentMetaItem : Plugins.MetaConversationItem {
this.requires_header = true; this.requires_header = true;
this.content_item = content_item; this.content_item = content_item;
this.widget_factory = widget_factory;
}
public override bool can_merge { get; set; default=true; }
public override bool requires_avatar { get; set; default=true; }
public override bool requires_header { get; set; default=true; }
public override Object? get_widget(Plugins.WidgetType type) {
return widget_factory.get_widget(content_item);
} }
} }

View file

@ -15,6 +15,8 @@ public class ConversationItemSkeleton : EventBox {
public StreamInteractor stream_interactor; public StreamInteractor stream_interactor;
public Conversation conversation { get; set; } public Conversation conversation { get; set; }
public Plugins.MetaConversationItem item; public Plugins.MetaConversationItem item;
public ContentMetaItem? content_meta_item = null;
public Widget? widget = null;
private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true };
private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true }; private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true };
@ -25,9 +27,18 @@ public class ConversationItemSkeleton : EventBox {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.conversation = conversation; this.conversation = conversation;
this.item = item; this.item = item;
this.content_meta_item = item as ContentMetaItem;
this.get_style_context().add_class("message-box"); this.get_style_context().add_class("message-box");
Widget? widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; item.notify["in-edit-mode"].connect(() => {
if (item.in_edit_mode) {
this.get_style_context().add_class("edit-mode");
} else {
this.get_style_context().remove_class("edit-mode");
}
});
widget = item.get_widget(Plugins.WidgetType.GTK) as Widget;
if (widget != null) { if (widget != null) {
widget.valign = Align.END; widget.valign = Align.END;
header_content_box.add(widget); header_content_box.add(widget);
@ -51,7 +62,12 @@ public class ConversationItemSkeleton : EventBox {
update_margin(); update_margin();
} }
public void update_margin() { public void set_edit_mode() {
if (content_meta_item == null) return;
}
private void update_margin() {
if (item.requires_header && show_skeleton && metadata_header == null) { if (item.requires_header && show_skeleton && metadata_header == null) {
metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true }; metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true };
header_content_box.add(metadata_header); header_content_box.add(metadata_header);

View file

@ -15,6 +15,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
[GtkChild] public ScrolledWindow scrolled; [GtkChild] public ScrolledWindow scrolled;
[GtkChild] private Revealer notification_revealer; [GtkChild] private Revealer notification_revealer;
[GtkChild] private Box message_menu_box; [GtkChild] private Box message_menu_box;
[GtkChild] private Button button1;
[GtkChild] private Image button1_icon;
[GtkChild] private Box notifications; [GtkChild] private Box notifications;
[GtkChild] private Box main; [GtkChild] private Box main;
[GtkChild] private EventBox main_event_box; [GtkChild] private EventBox main_event_box;
@ -29,7 +31,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>(); private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
private ContentProvider content_populator; private ContentProvider content_populator;
private SubscriptionNotitication subscription_notification; private SubscriptionNotitication subscription_notification;
private bool enable_menu_box = false;
private double? was_value; private double? was_value;
private double? was_upper; private double? was_upper;
@ -40,8 +41,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
private bool firstLoad = true; private bool firstLoad = true;
private bool at_current_content = true; private bool at_current_content = true;
private bool reload_messages = true; private bool reload_messages = true;
Widget currently_highlighted = null; ConversationItemSkeleton currently_highlighted = null;
ContentItem current_highlighted_item = null; ContentMetaItem? current_meta_item = null;
bool mouse_inside = false; bool mouse_inside = false;
int last_y_root = -1; int last_y_root = -1;
@ -67,6 +68,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
main_event_box.events = EventMask.POINTER_MOTION_MASK; main_event_box.events = EventMask.POINTER_MOTION_MASK;
main_event_box.motion_notify_event.connect(on_motion_notify_event); main_event_box.motion_notify_event.connect(on_motion_notify_event);
button1.clicked.connect(() => {
current_meta_item.get_item_actions(Plugins.WidgetType.GTK)[0].callback(button1, current_meta_item, currently_highlighted.widget);
update_message_menu();
});
return this; return this;
} }
@ -95,7 +101,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
} }
last_y_root = y_root; last_y_root = y_root;
message_menu_box.visible = enable_menu_box;
// Get pointer location in main // Get pointer location in main
int geometry_x, geometry_y, geometry_width, geometry_height, dest_x, dest_y; int geometry_x, geometry_y, geometry_width, geometry_height, dest_x, dest_y;
@ -106,21 +111,24 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
// Get widget under pointer // Get widget under pointer
int h = 0; int h = 0;
bool @break = false; bool @break = false;
Widget? w = null; ConversationItemSkeleton? w = null;
main.@foreach((widget) => { main.@foreach((widget) => {
if (break) return; if (break) return;
h += widget.get_allocated_height(); h += widget.get_allocated_height();
w = widget; w = widget as ConversationItemSkeleton;
if (h >= dest_y) { if (h >= dest_y) {
@break = true; @break = true;
return; return;
} }
}); });
if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT);
if (w == null) { if (w == null) {
if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT);
currently_highlighted = null; currently_highlighted = null;
current_meta_item = null;
update_message_menu();
return; return;
} }
@ -129,23 +137,36 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
w.translate_coordinates(main, 0, 0, out widget_x, out widget_y); w.translate_coordinates(main, 0, 0, out widget_x, out widget_y);
// Get MessageItem // Get MessageItem
var iter = widgets.map_iterator(); foreach (Plugins.MetaConversationItem item in item_item_skeletons.keys) {
while (iter.next()) { if (item_item_skeletons[item] == w) {
if (iter.get_value() == w) { current_meta_item = item as ContentMetaItem;
Plugins.MetaConversationItem meta_item = iter.get_key();
var meta_content_item = meta_item as ContentMetaItem;
if (meta_content_item == null) return;
current_highlighted_item = meta_content_item.content_item;
} }
} }
// Highlight widget update_message_menu();
if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT);
w.set_state_flags(StateFlags.PRELIGHT, true);
currently_highlighted = w;
// Move message menu if (current_meta_item != null) {
message_menu_box.margin_top = widget_y - 10; // Highlight widget
w.set_state_flags(StateFlags.PRELIGHT, true);
currently_highlighted = w;
// Move message menu
message_menu_box.margin_top = widget_y - 10;
}
}
private void update_message_menu() {
if (current_meta_item == null) {
message_menu_box.visible = false;
return;
}
var actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK);
message_menu_box.visible = actions != null && actions.size > 0;
if (actions != null && actions.size == 1) {
button1.visible = true;
button1_icon.set_from_icon_name(actions[0].icon_name, IconSize.SMALL_TOOLBAR);
}
} }
public void initialize_for_conversation(Conversation? conversation) { public void initialize_for_conversation(Conversation? conversation) {
@ -163,8 +184,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins
initialize_for_conversation_(conversation); initialize_for_conversation_(conversation);
display_latest(); display_latest();
stack.set_visible_child_name("main"); stack.set_visible_child_name("main");
enable_menu_box = false;
} }
public void initialize_around_message(Conversation conversation, ContentItem content_item) { public void initialize_around_message(Conversation conversation, ContentItem content_item) {

View file

@ -54,10 +54,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.Conver
public class MetaDateItem : Plugins.MetaConversationItem { public class MetaDateItem : Plugins.MetaConversationItem {
public override DateTime sort_time { get; set; } public override DateTime sort_time { get; set; }
public override bool can_merge { get; set; default=false; }
public override bool requires_avatar { get; set; default=false; }
public override bool requires_header { get; set; default=false; }
private DateTime date; private DateTime date;
public MetaDateItem(DateTime date) { public MetaDateItem(DateTime date) {
@ -76,6 +72,8 @@ public class MetaDateItem : Plugins.MetaConversationItem {
return box; return box;
} }
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
private static string get_relative_time(DateTime time) { private static string get_relative_time(DateTime time) {
DateTime time_local = time.to_local(); DateTime time_local = time.to_local();
DateTime now_local = new DateTime.now_local(); DateTime now_local = new DateTime.now_local();

View file

@ -7,6 +7,24 @@ using Dino.Entities;
namespace Dino.Ui.ConversationSummary { namespace Dino.Ui.ConversationSummary {
public class FileMetaItem : ContentMetaItem {
private StreamInteractor stream_interactor;
public FileMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
base(content_item);
this.stream_interactor = stream_interactor;
}
public override Object? get_widget(Plugins.WidgetType type) {
FileItem file_item = content_item as FileItem;
FileTransfer transfer = file_item.file_transfer;
return new FileWidget(stream_interactor, transfer) { visible=true };
}
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
}
public class FileWidget : Box { public class FileWidget : Box {
enum State { enum State {

View file

@ -0,0 +1,211 @@
using Gee;
using Gdk;
using Gtk;
using Pango;
using Xmpp;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class MessageMetaItem : ContentMetaItem {
private StreamInteractor stream_interactor;
private MessageItemWidget message_item_widget;
private MessageItem message_item;
public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
base(content_item);
message_item = content_item as MessageItem;
this.stream_interactor = stream_interactor;
}
public override Object? get_widget(Plugins.WidgetType type) {
message_item_widget = new MessageItemWidget(stream_interactor, content_item) { visible=true };
message_item_widget.edit_cancelled.connect(() => { this.in_edit_mode = false; });
message_item_widget.edit_sent.connect(on_edit_send);
stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction);
return message_item_widget;
}
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
if (content_item as FileItem != null) return null;
bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message);
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
if (allowed && !in_edit_mode) {
Plugins.MessageAction action1 = new Plugins.MessageAction();
action1.icon_name = "document-edit-symbolic";
action1.callback = (button, content_meta_item_activated, widget) => {
message_item_widget.set_edit_mode();
this.in_edit_mode = true;
};
actions.add(action1);
}
return actions;
}
private void on_edit_send(string text) {
stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text);
this.in_edit_mode = false;
}
private void on_received_correction(ContentItem content_item) {
if (this.content_item.id == content_item.id) {
this.content_item = content_item;
message_item = content_item as MessageItem;
message_item_widget.content_item = content_item;
message_item_widget.update_label();
}
}
}
public class MessageItemWidget : SizeRequestBin {
public signal void edit_cancelled();
public signal void edit_sent(string text);
StreamInteractor stream_interactor;
public ContentItem content_item;
Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true };
MessageItemEditMode? edit_mode = null;
ChatTextViewController? controller = null;
ulong realize_id = -1;
ulong style_updated_id = -1;
construct {
this.add(label);
this.size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
}
public MessageItemWidget(StreamInteractor stream_interactor, ContentItem content_item) {
this.stream_interactor = stream_interactor;
this.content_item = content_item;
update_label();
}
public void set_edit_mode() {
MessageItem message_item = content_item as MessageItem;
Message message = message_item.message;
if (edit_mode == null) {
edit_mode = new MessageItemEditMode();
controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor);
Conversation conversation = message_item.conversation;
controller.initialize_for_conversation(conversation);
edit_mode.cancelled.connect(() => {
edit_cancelled();
unset_edit_mode();
});
edit_mode.send.connect(() => {
edit_sent(edit_mode.chat_text_view.text_view.buffer.text);
unset_edit_mode();
});
}
edit_mode.chat_text_view.text_view.buffer.text = message.body;
this.remove(label);
this.add(edit_mode);
edit_mode.chat_text_view.text_view.grab_focus();
}
public void unset_edit_mode() {
this.remove(edit_mode);
this.add(label);
}
public void update_label() {
label.label = generate_markup_text(content_item);
}
private string generate_markup_text(ContentItem item) {
MessageItem message_item = item as MessageItem;
Conversation conversation = message_item.conversation;
Message message = message_item.message;
bool theme_dependent = false;
string markup_text = message.body;
if (markup_text.length > 10000) {
markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]";
}
if (message.body.has_prefix("/me")) {
markup_text = markup_text.substring(3);
}
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true);
} else {
markup_text = Util.parse_add_markup(markup_text, null, true, true);
}
if (message.body.has_prefix("/me")) {
string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(label));
markup_text = @"<span color=\"#$(color)\">$(Markup.escape_text(display_name))</span>" + markup_text;
theme_dependent = true;
}
int only_emoji_count = Util.get_only_emoji_count(markup_text);
if (only_emoji_count != -1) {
string size_str = only_emoji_count < 5 ? "xx-large" : "large";
markup_text = @"<span size=\'$size_str\'>" + markup_text + "</span>";
}
if (message.edit_to != null) {
string color = Util.is_dark_theme(label) ? "#808080" : "#909090";
markup_text += " <span size='small' color='%s'>(%s)</span>".printf(color, _("edited"));
theme_dependent = true;
}
if (theme_dependent && realize_id == -1) {
realize_id = label.realize.connect(update_label);
style_updated_id = label.style_updated.connect(update_label);
} else if (!theme_dependent && realize_id != -1) {
label.disconnect(realize_id);
label.disconnect(style_updated_id);
}
return markup_text;
}
}
[GtkTemplate (ui = "/im/dino/Dino/message_item_widget_edit_mode.ui")]
public class MessageItemEditMode : Box {
public signal void cancelled();
public signal void send();
[GtkChild] public MenuButton emoji_button;
[GtkChild] public ChatTextView chat_text_view;
[GtkChild] public Button cancel_button;
[GtkChild] public Button send_button;
[GtkChild] public Frame frame;
construct {
Util.force_css(frame, "* { border-radius: 3px; }");
EmojiChooser chooser = new EmojiChooser();
chooser.emoji_picked.connect((emoji) => {
chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length);
});
emoji_button.set_popover(chooser);
cancel_button.clicked.connect(() => cancelled());
send_button.clicked.connect(() => send());
chat_text_view.cancel_input.connect(() => cancelled());
chat_text_view.send_text.connect(() => send());
}
}
}

View file

@ -86,6 +86,12 @@ public class ConversationSelectorRow : ListBoxRow {
content_item_received(item); content_item_received(item);
} }
}); });
stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect((item) => {
if (last_content_item != null && last_content_item.id == item.id) {
content_item_received(item);
}
});
last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation);
x_button.clicked.connect(() => { x_button.clicked.connect(() => {

View file

@ -40,7 +40,6 @@ public class ConversationViewController : Object {
view.conversation_frame.init(stream_interactor); view.conversation_frame.init(stream_interactor);
// drag 'n drop file upload // drag 'n drop file upload
Gtk.drag_dest_unset(view.chat_input.text_input);
Gtk.drag_dest_set(view, DestDefaults.ALL, target_list, Gdk.DragAction.COPY); Gtk.drag_dest_set(view, DestDefaults.ALL, target_list, Gdk.DragAction.COPY);
view.drag_data_received.connect(this.on_drag_data_received); view.drag_data_received.connect(this.on_drag_data_received);
@ -52,7 +51,9 @@ public class ConversationViewController : Object {
// goto-end floating button // goto-end floating button
var vadjustment = view.conversation_frame.scrolled.vadjustment; var vadjustment = view.conversation_frame.scrolled.vadjustment;
vadjustment.notify["value"].connect(() => { vadjustment.notify["value"].connect(() => {
view.goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size; bool button_active = vadjustment.value < vadjustment.upper - vadjustment.page_size;
view.goto_end_revealer.reveal_child = button_active;
view.goto_end_revealer.visible = button_active;
}); });
view.goto_end_button.clicked.connect(() => { view.goto_end_button.clicked.connect(() => {
view.conversation_frame.initialize_for_conversation(conversation); view.conversation_frame.initialize_for_conversation(conversation);
@ -158,9 +159,11 @@ public class ConversationViewController : Object {
if ((event.state & ModifierType.CONTROL_MASK) > 0) { if ((event.state & ModifierType.CONTROL_MASK) > 0) {
return false; return false;
} }
view.chat_input.text_input.key_press_event(event); if (view.chat_input.chat_text_view.text_view.key_press_event(event)) {
view.chat_input.text_input.grab_focus(); view.chat_input.chat_text_view.text_view.grab_focus();
return true; return true;
}
return false;
} }
} }
} }

View file

@ -1,7 +1,15 @@
using Gtk; using Gtk;
namespace Dino.Ui { namespace Dino.Ui {
class SizeRequestBox : Box { public class SizeRequestBox : Box {
public SizeRequestMode size_request_mode { get; set; default = SizeRequestMode.CONSTANT_SIZE; }
public override Gtk.SizeRequestMode get_request_mode() {
return size_request_mode;
}
}
public class SizeRequestBin : Bin {
public SizeRequestMode size_request_mode { get; set; default = SizeRequestMode.CONSTANT_SIZE; } public SizeRequestMode size_request_mode { get; set; default = SizeRequestMode.CONSTANT_SIZE; }
public override Gtk.SizeRequestMode get_request_mode() { public override Gtk.SizeRequestMode get_request_mode() {

View file

@ -79,9 +79,6 @@ public class BadMessagesPopulator : Plugins.ConversationItemPopulator, Plugins.C
} }
public class BadMessageItem : Plugins.MetaConversationItem { public class BadMessageItem : Plugins.MetaConversationItem {
public override bool can_merge { get; set; default=false; }
public override bool requires_avatar { get; set; default=false; }
public override bool requires_header { get; set; default=false; }
private Plugin plugin; private Plugin plugin;
private Account account; private Account account;
@ -101,6 +98,8 @@ public class BadMessageItem : Plugins.MetaConversationItem {
public override Object? get_widget(Plugins.WidgetType widget_type) { public override Object? get_widget(Plugins.WidgetType widget_type) {
return new BadMessagesWidget(plugin, account, problem_jid, badness_type); return new BadMessagesWidget(plugin, account, problem_jid, badness_type);
} }
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
} }
public class BadMessagesWidget : Box { public class BadMessagesWidget : Box {

View file

@ -85,6 +85,7 @@ SOURCES
"src/module/xep/0260_jingle_socks5_bytestreams.vala" "src/module/xep/0260_jingle_socks5_bytestreams.vala"
"src/module/xep/0261_jingle_in_band_bytestreams.vala" "src/module/xep/0261_jingle_in_band_bytestreams.vala"
"src/module/xep/0280_message_carbons.vala" "src/module/xep/0280_message_carbons.vala"
"src/module/xep/0308_last_message_correction.vala"
"src/module/xep/0313_message_archive_management.vala" "src/module/xep/0313_message_archive_management.vala"
"src/module/xep/0333_chat_markers.vala" "src/module/xep/0333_chat_markers.vala"
"src/module/xep/0334_message_processing_hints.vala" "src/module/xep/0334_message_processing_hints.vala"

View file

@ -0,0 +1,31 @@
namespace Xmpp.Xep.LastMessageCorrection {
private const string NS_URI = "urn:xmpp:message-correct:0";
public static void set_replace_id(MessageStanza message, string replace_id) {
StanzaNode hint_node = (new StanzaNode.build("replace", NS_URI)).add_self_xmlns().put_attribute("id", replace_id);
message.stanza.put_node(hint_node);
}
public static string? get_replace_id(MessageStanza message) {
StanzaNode? node = message.stanza.get_subnode("replace", NS_URI);
if (node == null) return null;
return node.get_attribute("id");
}
public class Module : XmppStreamModule {
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0308_last_message_correction");
public override void attach(XmppStream stream) {
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
}
public override void detach(XmppStream stream) {}
public override string get_ns() { return NS_URI; }
public override string get_id() { return IDENTITY.id; }
}
}