Plugins providing conversation items for ConversationView

This commit is contained in:
fiaxh 2017-08-27 23:55:49 +02:00
parent a807ded65c
commit 8bc0d107e7
25 changed files with 713 additions and 525 deletions

View file

@ -16,8 +16,8 @@ SOURCES
src/dbus/upower.vala
src/entity/account.vala
src/entity/encryption.vala
src/entity/conversation.vala
src/entity/encryption.vala
src/entity/jid.vala
src/entity/message.vala
src/entity/settings.vala

View file

@ -71,4 +71,41 @@ public interface ConversationTitlebarWidget : Object {
public abstract void set_conversation(Conversation conversation);
}
public abstract interface ConversationItemPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type);
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
public virtual void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
public abstract void close(Conversation conversation);
}
public abstract class MetaConversationItem : Object {
public virtual Jid? jid { get; set; default=null; }
public virtual string color { get; set; default=null; }
public virtual string display_name { get; set; default=null; }
public virtual bool dim { get; set; default=false; }
public virtual DateTime? sort_time { get; set; default=null; }
public virtual DateTime? display_time { get; set; default=null; }
public virtual Encryption? encryption { get; set; default=null; }
public virtual Entities.Message.Marked? mark { get; set; default=null; }
public abstract bool can_merge { get; set; }
public abstract bool requires_avatar { get; set; }
public abstract bool requires_header { get; set; }
public abstract Object get_widget(WidgetType type);
}
public interface ConversationItemCollection : Object {
public abstract void insert_item(MetaConversationItem item);
public abstract void remove_item(MetaConversationItem item);
}
public interface MessageDisplayProvider : Object {
public abstract string id { get; set; }
public abstract double priority { get; set; }
public abstract bool can_display(Entities.Message? message);
public abstract MetaConversationItem? get_item(Entities.Message message, Entities.Conversation conversation);
}
}

View file

@ -7,6 +7,8 @@ public class Registry {
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
internal Gee.List<MessageDisplayProvider> message_displays = new ArrayList<MessageDisplayProvider>();
internal Gee.List<ConversationItemPopulator> conversation_item_populators = new ArrayList<ConversationItemPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
if (a.order < b.order) {
return -1;
@ -67,6 +69,26 @@ public class Registry {
return true;
}
}
public bool register_message_display(MessageDisplayProvider provider) {
lock (message_displays) {
foreach(MessageDisplayProvider p in message_displays) {
if (p.id == provider.id) return false;
}
message_displays.add(provider);
return true;
}
}
public bool register_conversation_item_populator(ConversationItemPopulator populator) {
lock (conversation_item_populators) {
foreach(ConversationItemPopulator p in conversation_item_populators) {
if (p.id == populator.id) return false;
}
conversation_item_populators.add(populator);
return true;
}
}
}
}

View file

@ -201,7 +201,7 @@ public class Database : Qlite.Database {
}
}
public Gee.List<Message> get_messages(Jid jid, Account account, Message.Type? type, int count, Message? before) {
public Gee.List<Message> get_messages(Jid jid, Account account, Message.Type? type, int count, DateTime? before) {
QueryBuilder select = message.select()
.with(message.counterpart_id, "=", get_jid_id(jid))
.with(message.account_id, "=", account.id)
@ -214,7 +214,7 @@ public class Database : Qlite.Database {
select.with(message.type_, "=", (int) type);
}
if (before != null) {
select.with(message.time, "<", (long) before.time.to_unix());
select.with(message.time, "<", (long) before.to_unix());
}
LinkedList<Message> ret = new LinkedList<Message>();

View file

@ -47,7 +47,7 @@ public class MessageStorage : StreamInteractionModule, Object {
return null;
}
public Gee.List<Message>? get_messages_before(Conversation? conversation, Message before, int count = 20) {
public Gee.List<Message>? get_messages_before(Conversation? conversation, DateTime before, int count = 20) {
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before);
return db_messages;
}

View file

@ -96,13 +96,13 @@ SOURCES
src/ui/conversation_selector/groupchat_row.vala
src/ui/conversation_selector/list.vala
src/ui/conversation_selector/view.vala
src/ui/conversation_summary/conversation_item.vala
src/ui/conversation_summary/merged_message_item.vala
src/ui/conversation_summary/message_item.vala
src/ui/conversation_summary/chat_state_populator.vala
src/ui/conversation_summary/conversation_item_skeleton.vala
src/ui/conversation_summary/conversation_view.vala
src/ui/conversation_summary/default_message_display.vala
src/ui/conversation_summary/message_populator.vala
src/ui/conversation_summary/message_textview.vala
src/ui/conversation_summary/slashme_item.vala
src/ui/conversation_summary/status_item.vala
src/ui/conversation_summary/view.vala
src/ui/conversation_summary/slashme_message_display.vala
src/ui/conversation_titlebar/encryption_entry.vala
src/ui/conversation_titlebar/menu_entry.vala
src/ui/conversation_titlebar/occupants_entry.vala

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="DinoUiConversationSummaryMessageItem">
<template class="DinoUiConversationSummaryConversationItemSkeleton">
<property name="hexpand">True</property>
<property name="column-spacing">7</property>
<property name="orientation">horizontal</property>
@ -23,8 +23,9 @@
</child>
<child>
<object class="GtkLabel" id="time_label">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="valign">start</property>
<property name="visible">True</property>
<style>
<class name="dim-label"/>
</style>
@ -40,6 +41,7 @@
<object class="GtkImage" id="encryption_image">
<property name="visible">False</property>
<property name="xalign">1</property>
<property name="valign">start</property>
<style>
<class name="dim-label"/>
</style>
@ -55,6 +57,7 @@
<object class="GtkImage" id="received_image">
<property name="visible">False</property>
<property name="xalign">1</property>
<property name="valign">start</property>
<style>
<class name="dim-label"/>
</style>
@ -67,4 +70,4 @@
</packing>
</child>
</template>
</interface>
</interface>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="DinoUiConversationSummaryView">
<template class="DinoUiConversationSummaryConversationView">
<property name="expand">True</property>
<property name="homogeneous">False</property>
<property name="spacing">0</property>

View file

@ -0,0 +1,116 @@
using Gee;
using Gtk;
using Dino.Entities;
using Xmpp;
namespace Dino.Ui.ConversationSummary {
class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
public string id { get { return "chat_state"; } }
private StreamInteractor? stream_interactor;
private Conversation? current_conversation;
private Plugins.ConversationItemCollection? item_collection;
private MetaChatStateItem? meta_item;
public ChatStatePopulator(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => {
if (current_conversation != null && current_conversation.account.equals(account) && current_conversation.counterpart.equals_bare(jid)) {
Idle.add(() => { update_chat_state(account, jid, state); return false; });
}
});
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
if (conversation.equals(current_conversation)) {
Idle.add(() => { update_chat_state(conversation.account, conversation.counterpart); return false; });
}
});
}
public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) {
current_conversation = conversation;
this.item_collection = item_collection;
this.meta_item = null;
update_chat_state(conversation.account, conversation.counterpart);
}
public void close(Conversation conversation) { }
public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
private void update_chat_state(Account account, Jid jid, string? state = null) {
string? state_ = state;
if (state_ == null) {
state_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_state(current_conversation.account, current_conversation.counterpart);
}
string? new_text = null;
if (state_ != null) {
if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
string display_name = Util.get_display_name(stream_interactor, jid, account);
if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) {
new_text = _("is typing...");
} else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
new_text = _("has stopped typing");
}
}
}
if (meta_item != null && new_text == null) {
item_collection.remove_item(meta_item);
meta_item = null;
} else if (meta_item != null && new_text != null) {
meta_item.set_text(new_text);
} else if (new_text != null) {
meta_item = new MetaChatStateItem(stream_interactor, current_conversation, jid, new_text);
item_collection.insert_item(meta_item);
}
}
}
public class MetaChatStateItem : Plugins.MetaConversationItem {
public override Jid? jid { get; set; }
public override bool dim { get; set; default=true; }
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=true; }
public override bool requires_header { get; set; default=false; }
private StreamInteractor stream_interactor;
private Conversation conversation;
private string text;
private Label label;
public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string text) {
this.stream_interactor = stream_interactor;
this.conversation = conversation;
this.jid = jid;
this.text = text;
}
public override Object get_widget(Plugins.WidgetType widget_type) {
label = new Label("") { xalign=0, vexpand=true, visible=true };
label.get_style_context().add_class("dim-label");
update_text();
return label;
}
public void set_text(string text) {
this.text = text;
update_text();
}
private void update_text() {
string display_name = Util.get_display_name(stream_interactor, jid, conversation.account);
label.label = display_name + " " + text;
}
}
}

View file

@ -1,32 +0,0 @@
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public enum MessageKind {
TEXT,
ME_COMMAND
}
public MessageKind get_message_kind(Message message) {
if (message.body.has_prefix("/me ")) {
return MessageKind.ME_COMMAND;
} else {
return MessageKind.TEXT;
}
}
public interface ConversationItem : Gtk.Widget {
public abstract bool merge(Entities.Message message);
public static ConversationItem create_for_message(StreamInteractor stream_interactor, Conversation conversation, Message message) {
switch (get_message_kind(message)) {
case MessageKind.TEXT:
return new MergedMessageItem(stream_interactor, conversation, message);
case MessageKind.ME_COMMAND:
return new SlashMeItem(stream_interactor, conversation, message);
}
assert_not_reached();
}
}
}

View file

@ -0,0 +1,142 @@
using Gee;
using Gdk;
using Gtk;
using Markup;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")]
public class ConversationItemSkeleton : Grid {
[GtkChild] private Image image;
[GtkChild] private Label time_label;
[GtkChild] private Image encryption_image;
[GtkChild] private Image received_image;
public StreamInteractor stream_interactor;
public Conversation conversation { get; set; }
public Gee.List<Plugins.MetaConversationItem> items = new ArrayList<Plugins.MetaConversationItem>();
private Box box = new Box(Orientation.VERTICAL, 2) { visible=true };
public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation) {
this.conversation = conversation;
this.stream_interactor = stream_interactor;
set_main_widget(box);
}
public void add_meta_item(Plugins.MetaConversationItem item) {
items.add(item);
if (items.size == 1) {
setup(item);
}
Widget widget = (Widget) item.get_widget(Plugins.WidgetType.GTK);
if (item.requires_header) {
box.add(widget);
} else {
set_title_widget(widget);
}
item.notify["mark"].connect_after(update_received);
update_received();
}
public void set_title_widget(Widget w) {
attach(w, 1, 0, 1, 1);
}
public void set_main_widget(Widget w) {
attach(w, 1, 1, 2, 1);
}
public void update_time() {
if (items.size > 0 && items[0].display_time != null) {
time_label.label = get_relative_time(items[0].display_time.to_local());
}
}
private void setup(Plugins.MetaConversationItem item) {
update_time();
Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).set_greyscale(item.dim).draw_jid(stream_interactor, item.jid, conversation.account));
if (item.requires_header) {
set_default_title_widget(item.jid);
}
if (item.encryption != null && item.encryption != Encryption.NONE) {
encryption_image.visible = true;
encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
}
}
private void set_default_title_widget(Jid jid) {
Label name_label = new Label("") { use_markup=true, xalign=0, hexpand=true, visible=true };
string display_name = Util.get_display_name(stream_interactor, jid, conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label));
name_label.label = @"<span foreground=\"#$color\">$display_name</span>";
name_label.style_updated.connect(() => {
string new_color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label));
name_label.set_markup(@"<span foreground=\"#$new_color\">$display_name</span>");
});
set_title_widget(name_label);
}
private void update_received() {
bool all_received = true;
bool all_read = true;
foreach (Plugins.MetaConversationItem item in items) {
if (item.mark == Message.Marked.WONTSEND) {
received_image.visible = true;
received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
Util.force_error_color(received_image);
Util.force_error_color(encryption_image);
Util.force_error_color(time_label);
return;
} else if (item.mark != Message.Marked.READ) {
all_read = false;
if (item.mark != Message.Marked.RECEIVED) {
all_received = false;
}
}
}
if (all_read) {
received_image.visible = true;
received_image.set_from_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
} else if (all_received) {
received_image.visible = true;
received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
} else if (received_image.visible) {
received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
}
}
private static string get_relative_time(DateTime datetime) {
DateTime now = new DateTime.now_local();
TimeSpan timespan = now.difference(datetime);
if (timespan > 365 * TimeSpan.DAY) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") :
/* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p"));
} else if (timespan > 7 * TimeSpan.DAY) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") :
/* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p"));
} else if (datetime.get_day_of_month() != new DateTime.now_utc().get_day_of_month()) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") :
/* xgettext:no-c-format */ _("%a, %l\u2236%M %p"));
} else if (timespan > 9 * TimeSpan.MINUTE) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") :
/* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p"));
} else if (timespan > TimeSpan.MINUTE) {
ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE);
/* xgettext:this is the beginning of a sentence. */
return n("%i min ago", "%i mins ago", mins).printf(mins);
} else {
return _("Just now");
}
}
}
}

View file

@ -0,0 +1,173 @@
using Gee;
using Gtk;
using Pango;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")]
public class ConversationView : Box, Plugins.ConversationItemCollection {
public Conversation? conversation { get; private set; }
[GtkChild] private ScrolledWindow scrolled;
[GtkChild] private Box main;
[GtkChild] private Stack stack;
private StreamInteractor stream_interactor;
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>((a, b) => { return a.sort_time.compare(b.sort_time); });
private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>();
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
private MessagePopulator message_item_populator;
private double? was_value;
private double? was_upper;
private double? was_page_size;
private Mutex reloading_mutex = new Mutex();
private bool animate = false;
public ConversationView(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
scrolled.vadjustment.notify["value"].connect(on_value_notify);
message_item_populator = new MessagePopulator(stream_interactor);
Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor));
Timeout.add_seconds(60, () => {
foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
item_skeleton.update_time();
}
return true;
});
Util.force_base_background(this);
}
public void initialize_for_conversation(Conversation? conversation) {
this.conversation = conversation;
stack.set_visible_child_name("void");
clear();
was_upper = null;
was_page_size = null;
animate = false;
Timeout.add(20, () => { animate = true; return false; });
message_item_populator.init(conversation, this);
message_item_populator.populate_number(conversation, new DateTime.now_utc(), 50);
Dino.Application app = Dino.Application.get_default();
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK);
}
stack.set_visible_child_name("main");
}
public void insert_item(Plugins.MetaConversationItem item) {
meta_items.add(item);
if (!item.can_merge || !merge_back(item)) {
insert_new(item);
}
}
public void remove_item(Plugins.MetaConversationItem item) {
main.remove(widgets[item]);
widgets.unset(item);
meta_items.remove(item);
item_skeletons.remove(item_item_skeletons[item]);
item_item_skeletons.unset(item);
}
private bool merge_back(Plugins.MetaConversationItem item) {
Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
if (lower_item != null) {
ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item];
Plugins.MetaConversationItem lower_start_item = lower_skeleton.items[0];
if (lower_start_item.can_merge &&
item.display_time.difference(lower_start_item.display_time) < TimeSpan.MINUTE &&
lower_start_item.jid.equals(item.jid) &&
lower_start_item.encryption == item.encryption &&
item.mark != Message.Marked.WONTSEND) {
lower_skeleton.add_meta_item(item);
force_alloc_width(lower_skeleton, main.get_allocated_width());
item_item_skeletons[item] = lower_skeleton;
return true;
}
}
return false;
}
private void insert_new(Plugins.MetaConversationItem item) {
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation);
item_skeleton.add_meta_item(item);
item_item_skeletons[item] = item_skeleton;
Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
item_skeletons.insert(index, item_skeleton);
Widget insert = item_skeleton;
if (animate) {
Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
revealer.add(item_skeleton);
insert = revealer;
main.add(insert);
revealer.reveal_child = true;
} else {
main.add(insert);
}
widgets[item] = insert;
force_alloc_width(insert, main.get_allocated_width());
main.reorder_child(insert, index);
}
private void on_upper_notify() {
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 ||
scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
} else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
}
was_upper = scrolled.vadjustment.upper;
was_page_size = scrolled.vadjustment.page_size;
reloading_mutex.trylock();
reloading_mutex.unlock();
}
private void on_value_notify() {
if (scrolled.vadjustment.value < 200) {
load_earlier_messages();
}
}
private void load_earlier_messages() {
was_value = scrolled.vadjustment.value;
if (!reloading_mutex.trylock()) return;
if (meta_items.size > 0) message_item_populator.populate_number(conversation, meta_items.first().sort_time, 20);
}
// Workaround GTK TextView issues
private void force_alloc_width(Widget widget, int width) {
Allocation alloc = Allocation();
widget.get_preferred_width(out alloc.width, null);
widget.get_preferred_height(out alloc.height, null);
alloc.width = width;
widget.size_allocate(alloc);
}
private void clear() {
meta_items.clear();
meta_after_items.clear();
item_skeletons.clear();
item_item_skeletons.clear();
main.@foreach((widget) => { main.remove(widget); });
}
}
}

View file

@ -0,0 +1,53 @@
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class DefaultMessageDisplay : Plugins.MessageDisplayProvider, Object {
public string id { get; set; default="default"; }
public double priority { get; set; default=0; }
public StreamInteractor stream_interactor;
public DefaultMessageDisplay(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public bool can_display(Entities.Message? message) { return true; }
public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
return new MetaMessageItem(stream_interactor, message, conversation);
}
}
public class MetaMessageItem : 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; }
private StreamInteractor stream_interactor;
private Conversation conversation;
private Message message;
public MetaMessageItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
this.stream_interactor = stream_interactor;
this.conversation = conversation;
this.message = message;
this.jid = message.from;
this.sort_time = message.local_time;
this.display_time = message.time;
this.encryption = message.encryption;
}
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 widget_type) {
MessageTextView text_view = new MessageTextView() { visible = true };
text_view.add_text(message.body);
return text_view;
}
}
}

View file

@ -1,59 +0,0 @@
using Gee;
using Gdk;
using Gtk;
using Markup;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class MergedMessageItem : MessageItem {
private Label name_label = new Label("") { xalign=0, visible=true, hexpand=true };
private MessageTextView textview = new MessageTextView() { visible=true };
public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
base(stream_interactor, conversation, message);
set_main_widget(textview);
set_title_widget(name_label);
add_message(message);
string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.from, false);
name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>");
textview.style_updated.connect(update_display_style);
update_display_style();
}
public override void add_message(Message message) {
base.add_message(message);
if (messages.size > 1) textview.add_text("\n");
string text = message.body;
if (text.length > 10000) {
text = text.slice(0, 10000) + " [" + _("Message too long") + "]";
}
textview.add_text(text);
}
public override bool merge(Message message) {
if (get_message_kind(message) == MessageKind.TEXT &&
this.from.equals(message.from) &&
this.messages[0].encryption == message.encryption &&
message.time.difference(initial_time) < TimeSpan.MINUTE &&
this.messages[0].marked != Entities.Message.Marked.WONTSEND) {
add_message(message);
return true;
}
return false;
}
private void update_display_style() {
string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview));
name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>");
}
}
}

View file

@ -1,122 +0,0 @@
using Gee;
using Gdk;
using Gtk;
using Markup;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")]
public class MessageItem : Grid, ConversationItem {
[GtkChild] private Image image;
[GtkChild] private Label time_label;
[GtkChild] private Image encryption_image;
[GtkChild] private Image received_image;
public StreamInteractor stream_interactor;
public Conversation conversation { get; set; }
public Jid from { get; private set; }
public DateTime initial_time { get; private set; }
public ArrayList<Message> messages = new ArrayList<Message>(Message.equals_func);
public MessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
this.conversation = conversation;
this.stream_interactor = stream_interactor;
this.initial_time = message.time;
this.from = message.from;
if (message.encryption != Encryption.NONE) {
encryption_image.visible = true;
encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
}
time_label.label = get_relative_time(initial_time.to_local());
Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
}
public void set_title_widget(Widget w) {
attach(w, 1, 0, 1, 1);
}
public void set_main_widget(Widget w) {
attach(w, 1, 1, 2, 1);
}
public void update() {
time_label.label = get_relative_time(initial_time.to_local());
}
public virtual void add_message(Message message) {
messages.add(message);
message.notify["marked"].connect_after(() => {
Idle.add(() => { update_received(); return false; });
});
update_received();
}
public virtual bool merge(Message message) {
return false;
}
private void update_received() {
bool all_received = true;
bool all_read = true;
foreach (Message message in messages) {
if (message.marked == Message.Marked.WONTSEND) {
received_image.visible = true;
received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
Util.force_error_color(received_image);
Util.force_error_color(encryption_image);
Util.force_error_color(time_label);
return;
} else if (message.marked != Message.Marked.READ) {
all_read = false;
if (message.marked != Message.Marked.RECEIVED) {
all_received = false;
}
}
}
if (all_read) {
received_image.visible = true;
received_image.set_from_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
} else if (all_received) {
received_image.visible = true;
received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
} else if (received_image.visible) {
received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
}
}
private static string get_relative_time(DateTime datetime) {
DateTime now = new DateTime.now_local();
TimeSpan timespan = now.difference(datetime);
if (timespan > 365 * TimeSpan.DAY) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") :
/* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p"));
} else if (timespan > 7 * TimeSpan.DAY) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") :
/* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p"));
} else if (timespan > 1 * TimeSpan.DAY) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") :
/* xgettext:no-c-format */ _("%a, %l\u2236%M %p"));
} else if (timespan > 9 * TimeSpan.MINUTE) {
return datetime.format(Util.is_24h_format() ?
/* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") :
/* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p"));
} else if (timespan > TimeSpan.MINUTE) {
ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE);
/* xgettext:this is the beginning of a sentence. */
return n("%i min ago", "%i mins ago", mins).printf(mins);
} else {
return _("Just now");
}
}
}
}

View file

@ -0,0 +1,66 @@
using Gee;
using Gtk;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class MessagePopulator : Object {
private StreamInteractor? stream_interactor;
private Conversation? current_conversation;
private Plugins.ConversationItemCollection? item_collection;
public MessagePopulator(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_message_display(new DefaultMessageDisplay(stream_interactor));
app.plugin_registry.register_message_display(new SlashmeMessageDisplay(stream_interactor));
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => {
Idle.add(() => { handle_message(message, conversation); return false; });
});
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
Idle.add(() => { handle_message(message, conversation); return false; });
});
}
public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection) {
current_conversation = conversation;
this.item_collection = item_collection;
}
public void close(Conversation conversation) { }
public void populate_number(Conversation conversation, DateTime from, int n) {
Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before(conversation, from, n);
if (messages != null) {
foreach (Entities.Message message in messages) {
handle_message(message, conversation);
}
}
}
private void handle_message(Message message, Conversation conversation) {
if (!conversation.equals(current_conversation)) return;
Plugins.MessageDisplayProvider? best_provider = null;
int priority = -1;
Application app = GLib.Application.get_default() as Application;
foreach (Plugins.MessageDisplayProvider provider in app.plugin_registry.message_displays) {
if (provider.can_display(message) && provider.priority > priority) {
best_provider = provider;
}
}
Plugins.MetaConversationItem meta_item = best_provider.get_item(message, conversation);
meta_item.mark = message.marked;
message.notify["marked"].connect(() => {
meta_item.mark = message.marked;
});
item_collection.insert_item(meta_item);
}
}
}

View file

@ -27,7 +27,11 @@ public class MessageTextView : TextView {
minimum_width = 0;
}
public void add_text(string text) {
public void add_text(string text_) {
string text = text_;
if (text.length > 10000) {
text = text.slice(0, 10000) + " [" + _("Message too long") + "]";
}
TextIter end;
buffer.get_end_iter(out end);
buffer.insert(ref end, text, -1);
@ -90,4 +94,4 @@ public class MessageTextView : TextView {
}
}
}
}

View file

@ -1,44 +0,0 @@
using Gdk;
using Gtk;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class SlashMeItem : MessageItem {
private Box box = new Box(Orientation.VERTICAL, 0) { visible=true, vexpand=true };
private MessageTextView textview = new MessageTextView() { visible=true };
private string text;
private TextTag nick_tag;
public SlashMeItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
base(stream_interactor, conversation, message);
box.set_center_widget(textview);
set_title_widget(box);
text = message.body.substring(3);
string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, false);
nick_tag = textview.buffer.create_tag("nick", foreground: "#" + color);
TextIter iter;
textview.buffer.get_start_iter(out iter);
textview.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
textview.add_text(text);
add_message(message);
textview.style_updated.connect(update_display_style);
update_display_style();
}
public override bool merge(Message message) {
return false;
}
private void update_display_style() {
string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview));
nick_tag.foreground = "#" + color;
}
}
}

View file

@ -0,0 +1,75 @@
using Gtk;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
public class SlashmeMessageDisplay : Plugins.MessageDisplayProvider, Object {
public string id { get; set; default="slashme"; }
public double priority { get; set; default=1; }
public StreamInteractor stream_interactor;
public SlashmeMessageDisplay(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public bool can_display(Entities.Message? message) {
return message.body.has_prefix("/me");
}
public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
return new MetaSlashmeItem(stream_interactor, message, conversation);
}
}
public class MetaSlashmeItem : 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; }
private StreamInteractor stream_interactor;
private Conversation conversation;
private Message message;
private TextTag nick_tag;
private MessageTextView text_view;
public MetaSlashmeItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
this.stream_interactor = stream_interactor;
this.conversation = conversation;
this.message = message;
this.jid = message.from;
this.sort_time = message.local_time;
this.display_time = message.time;
this.encryption = message.encryption;
}
public override bool can_merge { get; set; default=false; }
public override bool requires_avatar { get; set; default=true; }
public override bool requires_header { get; set; default=false; }
public override Object get_widget(Plugins.WidgetType widget_type) {
text_view = new MessageTextView() { valign=Align.CENTER, vexpand=true, visible = true };
string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view));
nick_tag = text_view.buffer.create_tag("nick", foreground: "#" + color);
TextIter iter;
text_view.buffer.get_start_iter(out iter);
text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
text_view.add_text(message.body.substring(3));
text_view.style_updated.connect(update_style);
text_view.realize.connect(update_style);
return text_view;
}
private void update_style() {
string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view));
nick_tag.foreground = "#" + color;
}
}
}

View file

@ -1,30 +0,0 @@
using Gtk;
using Markup;
using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
private class StatusItem : Grid {
private Image image = new Image();
private Label label = new Label("");
private StreamInteractor stream_interactor;
private Conversation conversation;
public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) {
Object(column_spacing : 7);
set_hexpand(true);
this.stream_interactor = stream_interactor;
this.conversation = conversation;
image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation));
attach(image, 0, 0, 1, 1);
attach(label, 1, 0, 1, 1);
string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account);
label.set_markup(@"<span foreground=\"#B1B1B1\">$(escape_text(display_name)) $text</span>");
show_all();
}
}
}

View file

@ -1,216 +0,0 @@
using Gee;
using Gtk;
using Pango;
using Dino.Entities;
using Xmpp;
namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")]
public class View : Box {
public Conversation? conversation { get; private set; }
public HashMap<Entities.Message, ConversationItem> conversation_items = new HashMap<Entities.Message, ConversationItem>(Entities.Message.hash_func, Entities.Message.equals_func);
[GtkChild] private ScrolledWindow scrolled;
[GtkChild] private Box main;
[GtkChild] private Stack stack;
private StreamInteractor stream_interactor;
private ConversationItem? last_conversation_item;
private StatusItem typing_status;
private Entities.Message? earliest_message;
double? was_value;
double? was_upper;
double? was_page_size;
Object reloading_lock = new Object();
bool reloading = false;
public View(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
scrolled.vadjustment.notify["value"].connect(on_value_notify);
stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => {
Idle.add(() => { on_received_state(account, jid, state); return false; });
});
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => {
Idle.add(() => { show_message(message, conversation, true); return false; });
});
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
Idle.add(() => { show_message(message, conversation, true); return false; });
});
stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((show, jid, account) => {
Idle.add(() => { on_show_received(show, jid, account); return false; });
});
Timeout.add_seconds(60, () => {
foreach (ConversationItem conversation_item in conversation_items.values) {
MessageItem message_item = conversation_item as MessageItem;
if (message_item != null) message_item.update();
}
return true;
});
Util.force_base_background(this);
}
public void initialize_for_conversation(Conversation? conversation) {
this.conversation = conversation;
stack.set_visible_child_name("void");
clear();
conversation_items.clear();
was_upper = null;
was_page_size = null;
last_conversation_item = null;
ArrayList<Object> objects = new ArrayList<Object>();
Gee.List<Entities.Message> messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation);
if (messages.size > 0) {
earliest_message = messages[messages.size -1];
objects.add_all(messages);
}
HashMap<Jid, ArrayList<Show>>? shows = stream_interactor.get_module(PresenceManager.IDENTITY).get_shows(conversation.counterpart, conversation.account);
if (shows != null) {
foreach (Jid jid in shows.keys) objects.add_all(shows[jid]);
}
objects.sort((a, b) => {
DateTime? dt1 = null;
DateTime? dt2 = null;
Entities.Message m1 = a as Entities.Message;
if (m1 != null) dt1 = m1.time;
Show s1 = a as Show;
if (s1 != null) dt1 = s1.datetime;
Entities.Message m2 = b as Entities.Message;
if (m2 != null) dt2 = m2.time;
Show s2 = b as Show;
if (s2 != null) dt2 = s2.datetime;
return dt1.compare(dt2);
});
foreach (Object o in objects) {
Entities.Message message = o as Entities.Message;
Show show = o as Show;
if (message != null) {
show_message(message, conversation);
} else if (show != null) {
on_show_received(show, conversation.counterpart, conversation.account);
}
}
update_chat_state();
stack.set_visible_child_name("main");
}
private void on_received_state(Account account, Jid jid, string state) {
if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) {
update_chat_state(state);
}
}
private void update_chat_state(string? state = null) {
string? state_ = state;
if (state_ == null) {
state_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_state(conversation.account, conversation.counterpart);
}
if (typing_status != null) {
main.remove(typing_status);
}
if (state_ != null) {
if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) {
typing_status = new StatusItem(stream_interactor, conversation, _("is typing…"));
} else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
typing_status = new StatusItem(stream_interactor, conversation, _("has stopped typing"));
}
main.add(typing_status);
}
}
}
private void on_show_received(Show show, Jid jid, Account account) {
}
private void on_upper_notify() {
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 ||
scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
} else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
}
was_upper = scrolled.vadjustment.upper;
was_page_size = scrolled.vadjustment.page_size;
lock(reloading_lock) {
reloading = false;
}
}
private void on_value_notify() {
if (scrolled.vadjustment.value < 200) {
load_earlier_messages();
}
}
private void load_earlier_messages() {
if (earliest_message == null) return;
was_value = scrolled.vadjustment.value;
lock(reloading_lock) {
if(reloading) return;
reloading = true;
}
Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before(conversation, earliest_message);
if (messages != null && messages.size > 0) {
earliest_message = messages[0];
MergedMessageItem? current_item = null;
int items_added = 0;
for (int i = 0; i < messages.size; i++) {
if (current_item == null || !current_item.merge(messages[i])) {
current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]);
force_alloc_width(current_item, main.get_allocated_width());
main.add(current_item);
conversation_items[messages[i]] = current_item;
main.reorder_child(current_item, items_added);
items_added++;
}
}
return;
}
reloading = false;
}
private void show_message(Entities.Message message, Conversation conversation, bool animate = false) {
if (this.conversation != null && this.conversation.equals(conversation)) {
if (last_conversation_item == null || !last_conversation_item.merge(message)) {
ConversationItem conversation_item = ConversationItem.create_for_message(stream_interactor, conversation, message);
if (animate) {
Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
revealer.add(conversation_item);
force_alloc_width(revealer, main.get_allocated_width());
main.add(revealer);
revealer.set_reveal_child(true);
} else {
force_alloc_width(conversation_item, main.get_allocated_width());
main.add(conversation_item);
}
last_conversation_item = conversation_item;
}
conversation_items[message] = last_conversation_item;
update_chat_state();
}
}
// Workaround GTK TextView issues
private void force_alloc_width(Widget widget, int width) {
Allocation alloc = Allocation();
widget.get_preferred_width(out alloc.width, null);
widget.get_preferred_height(out alloc.height, null);
alloc.width = width;
widget.size_allocate(alloc);
}
private void clear() {
main.@foreach((widget) => { main.remove(widget); });
}
}
}

View file

@ -12,7 +12,7 @@ public class UnifiedWindow : Window {
private ChatInput.View chat_input;
private ConversationListTitlebar conversation_list_titlebar;
private ConversationSelector.View filterable_conversation_list;
private ConversationSummary.View conversation_frame;
private ConversationSummary.ConversationView conversation_frame;
private ConversationTitlebar conversation_titlebar;
private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true };
private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true };
@ -69,7 +69,7 @@ public class UnifiedWindow : Window {
private void setup_unified() {
chat_input = new ChatInput.View(stream_interactor) { visible=true };
conversation_frame = new ConversationSummary.View(stream_interactor) { visible=true };
conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true };
filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true };
Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true };

View file

@ -119,8 +119,8 @@ public static void force_error_color(Gtk.Widget widget, string selector = "*") {
}
public static bool is_dark_theme(Gtk.Widget widget) {
Gdk.RGBA bg = widget.get_style_context().get_background_color(StateFlags.NORMAL);
return (bg.red < 0.5 && bg.green < 0.5 && bg.blue < 0.5);
Gdk.RGBA bg = widget.get_style_context().get_color(StateFlags.NORMAL);
return (bg.red > 0.5 && bg.green > 0.5 && bg.blue > 0.5);
}
public static bool is_24h_format() {

View file

@ -5,7 +5,7 @@ private const string NS_URI = "vcard-temp";
private const string NS_URI_UPDATE = NS_URI + ":x:update";
public class Module : XmppStreamModule {
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0027_current_pgp_usage");
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0153_vcard_based_avatars");
public signal void received_avatar(XmppStream stream, string jid, string id);

View file

@ -5,7 +5,7 @@ namespace Xmpp.Xep.StreamManagement {
public const string NS_URI = "urn:xmpp:sm:3";
public class Module : XmppStreamNegotiationModule {
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0313_message_archive_management");
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0198_stream_management");
public int h_inbound { get; private set; default=0; }
public string? session_id { get; set; default=null; }