load+display later messages when scrolling down

This commit is contained in:
bobufa 2018-06-23 11:59:21 +02:00
parent 443e7ee49d
commit 760fd4cb26
6 changed files with 147 additions and 67 deletions

View file

@ -46,50 +46,73 @@ public class ContentItemAccumulator : StreamInteractionModule, Object {
items.add(new FileItem(transfer)); items.add(new FileItem(transfer));
} }
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
if (items.size == 0) return ret;
BidirIterator<ContentItem> iter = items.bidir_iterator(); BidirIterator<ContentItem> iter = items.bidir_iterator();
iter.last(); iter.last();
int i = 0; int i = 0;
while (i < n && iter.has_previous()) { while (i < n - 1 && iter.has_previous()) {
iter.previous(); iter.previous();
i++; i++;
} }
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
do { do {
ret.add(iter.get()); ret.add(iter.get());
} while(iter.next()); } while (iter.next());
return ret; return ret;
} }
public Gee.List<ContentItem> populate_before(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) { public Gee.List<ContentItem> populate_before(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare); Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare);
Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, n); int before_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1;
Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, item.display_time, before_id, n);
if (messages != null) { if (messages != null) {
foreach (Entities.Message message in messages) { foreach (Entities.Message message in messages) {
items.add(new MessageItem(message, conversation)); items.add(new MessageItem(message, conversation));
} }
} }
Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.display_time, n); Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_before(conversation.account, conversation.counterpart, item.sort_time, n);
foreach (FileTransfer transfer in transfers) { foreach (FileTransfer transfer in transfers) {
items.add(new FileItem(transfer)); items.add(new FileItem(transfer));
} }
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
if (items.size == 0) return ret;
BidirIterator<ContentItem> iter = items.bidir_iterator(); BidirIterator<ContentItem> iter = items.bidir_iterator();
iter.last(); iter.last();
int i = 0; int i = 0;
while (i < n && iter.has_previous()) { while (i < n - 1 && iter.has_previous()) {
iter.previous(); iter.previous();
i++; i++;
} }
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
do { do {
ret.add(iter.get()); ret.add(iter.get());
} while(iter.next()); } while (iter.next());
return ret; return ret;
} }
public void populate_after(Conversation conversation, ContentItem item, int n) { public Gee.List<ContentItem> populate_after(ContentItemCollection item_collection, Conversation conversation, ContentItem item, int n) {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare);
int after_id = item as MessageItem != null ? (int)Math.floor(item.seccondary_sort_indicator) : -1;
Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(conversation, item.sort_time, after_id, n);
if (messages != null) {
foreach (Entities.Message message in messages) {
items.add(new MessageItem(message, conversation));
}
}
Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_transfers_after(conversation.account, conversation.counterpart, item.sort_time, n);
foreach (FileTransfer transfer in transfers) {
items.add(new FileItem(transfer));
}
Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
foreach (ContentItem content_item in items) {
ret.add(content_item);
}
return ret;
} }
public void add_filter(ContentFilter content_filter) { public void add_filter(ContentFilter content_filter) {
@ -196,7 +219,7 @@ public class FileItem : ContentItem {
this.file_transfer = file_transfer; this.file_transfer = file_transfer;
this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart;
this.sort_time = file_transfer.time; this.sort_time = file_transfer.local_time;
this.seccondary_sort_indicator = file_transfer.id + 0.2903; this.seccondary_sort_indicator = file_transfer.id + 0.2903;
this.display_time = file_transfer.time; this.display_time = file_transfer.time;
this.encryption = file_transfer.encryption; this.encryption = file_transfer.encryption;

View file

@ -236,11 +236,10 @@ public class Database : Qlite.Database {
} }
} }
public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before) { public Gee.List<Message> get_messages(Xmpp.Jid jid, Account account, Message.Type? type, int count, DateTime? before, DateTime? after, int id) {
QueryBuilder select = message.select() QueryBuilder select = message.select()
.with(message.counterpart_id, "=", get_jid_id(jid)) .with(message.counterpart_id, "=", get_jid_id(jid))
.with(message.account_id, "=", account.id) .with(message.account_id, "=", account.id)
.order_by(message.id, "DESC")
.limit(count); .limit(count);
if (jid.resourcepart != null) { if (jid.resourcepart != null) {
select.with(message.counterpart_resource, "=", jid.resourcepart); select.with(message.counterpart_resource, "=", jid.resourcepart);
@ -250,6 +249,17 @@ public class Database : Qlite.Database {
} }
if (before != null) { if (before != null) {
select.with(message.local_time, "<", (long) before.to_unix()); select.with(message.local_time, "<", (long) before.to_unix());
if (id > 0) {
select.with(message.id, "<", id);
}
}
if (after != null) {
select.with(message.local_time, ">", (long) after.to_unix());
if (id > 0) {
select.with(message.id, ">", id);
}
} else {
select.order_by(message.id, "DESC");
} }
LinkedList<Message> ret = new LinkedList<Message>(); LinkedList<Message> ret = new LinkedList<Message>();

View file

@ -84,13 +84,7 @@ public class FileManager : StreamInteractionModule, Object {
.with(db.file_transfer.account_id, "=", account.id) .with(db.file_transfer.account_id, "=", account.id)
.order_by(db.file_transfer.local_time, "DESC") .order_by(db.file_transfer.local_time, "DESC")
.limit(n); .limit(n);
return get_transfers_from_qry(select);
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
foreach (Qlite.Row row in select) {
FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());
ret.insert(0, file_transfer);
}
return ret;
} }
public Gee.List<FileTransfer> get_transfers_before(Account account, Jid counterpart, DateTime before, int n) { public Gee.List<FileTransfer> get_transfers_before(Account account, Jid counterpart, DateTime before, int n) {
@ -100,23 +94,19 @@ public class FileManager : StreamInteractionModule, Object {
.with(db.file_transfer.local_time, "<", (long)before.to_unix()) .with(db.file_transfer.local_time, "<", (long)before.to_unix())
.order_by(db.file_transfer.local_time, "DESC") .order_by(db.file_transfer.local_time, "DESC")
.limit(n); .limit(n);
return get_transfers_from_qry(select);
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
foreach (Qlite.Row row in select) {
FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());
ret.insert(0, file_transfer);
}
return ret;
} }
public Gee.List<FileTransfer> get_file_transfers(Account account, Jid counterpart, DateTime after, DateTime before) { public Gee.List<FileTransfer> get_transfers_after(Account account, Jid counterpart, DateTime after, int n) {
Qlite.QueryBuilder select = db.file_transfer.select() Qlite.QueryBuilder select = db.file_transfer.select()
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart)) .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(counterpart))
.with(db.file_transfer.account_id, "=", account.id) .with(db.file_transfer.account_id, "=", account.id)
.with(db.file_transfer.local_time, ">", (long)after.to_unix()) .with(db.file_transfer.local_time, ">", (long)after.to_unix())
.with(db.file_transfer.local_time, "<", (long)before.to_unix()) .limit(n);
.order_by(db.file_transfer.id, "DESC"); return get_transfers_from_qry(select);
}
private Gee.List<FileTransfer> get_transfers_from_qry(Qlite.QueryBuilder select) {
Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>(); Gee.List<FileTransfer> ret = new ArrayList<FileTransfer>();
foreach (Qlite.Row row in select) { foreach (Qlite.Row row in select) {
FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir()); FileTransfer file_transfer = new FileTransfer.from_row(db, row, get_storage_dir());

View file

@ -51,7 +51,7 @@ public class MessageStorage : StreamInteractionModule, Object {
return null; return null;
} }
public Gee.List<Message>? get_messages_before_message(Conversation? conversation, DateTime before, int count = 20) { public Gee.List<Message>? get_messages_before_message(Conversation? conversation, DateTime before, int id, int count = 20) {
// SortedSet<Message>? before = messages[conversation].head_set(message); // SortedSet<Message>? before = messages[conversation].head_set(message);
// if (before != null && before.size >= count) { // if (before != null && before.size >= count) {
// Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func); // Gee.List<Message> ret = new ArrayList<Message>(Message.equals_func);
@ -65,11 +65,16 @@ public class MessageStorage : StreamInteractionModule, Object {
// } // }
// return ret; // return ret;
// } else { // } else {
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before); Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, before, null, id);
return db_messages; return db_messages;
// } // }
} }
public Gee.List<Message>? get_messages_after_message(Conversation? conversation, DateTime after, int id, int count = 20) {
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), count, null, after, id);
return db_messages;
}
public Message? get_message_by_id(string stanza_id, Conversation conversation) { public Message? get_message_by_id(string stanza_id, Conversation conversation) {
init_conversation(conversation); init_conversation(conversation);
foreach (Message message in messages[conversation]) { foreach (Message message in messages[conversation]) {
@ -100,7 +105,7 @@ public class MessageStorage : StreamInteractionModule, Object {
} }
return res; return res;
}); });
Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null); Gee.List<Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, Util.get_message_type_for_conversation(conversation), 50, null, null, -1);
messages[conversation].add_all(db_messages); messages[conversation].add_all(db_messages);
} }
} }

View file

@ -53,6 +53,18 @@ public class ContentProvider : ContentItemCollection, Object {
} }
return ret; return ret;
} }
public Gee.List<ContentMetaItem> populate_after(Conversation conversation, Plugins.MetaConversationItem before_item, int n) {
Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
ContentMetaItem? content_meta_item = before_item as ContentMetaItem;
if (content_meta_item != null) {
Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemAccumulator.IDENTITY).populate_after(this, conversation, content_meta_item.content_item, n);
foreach (ContentItem item in items) {
ret.add(new ContentMetaItem(item, widget_factory));
}
}
return ret;
}
} }
public class ContentMetaItem : Plugins.MetaConversationItem { public class ContentMetaItem : Plugins.MetaConversationItem {

View file

@ -18,7 +18,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
[GtkChild] private Stack stack; [GtkChild] private Stack stack;
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items); private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>(); 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.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>(); private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
@ -32,6 +33,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
private Mutex reloading_mutex = Mutex(); private Mutex reloading_mutex = Mutex();
private bool animate = false; private bool animate = false;
private bool firstLoad = true; private bool firstLoad = true;
private bool at_current_content = true;
public ConversationView(StreamInteractor stream_interactor) { public ConversationView(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
@ -41,8 +43,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
content_populator = new ContentProvider(stream_interactor); content_populator = new ContentProvider(stream_interactor);
subscription_notification = new SubscriptionNotitication(stream_interactor); subscription_notification = new SubscriptionNotitication(stream_interactor);
insert_item.connect(on_insert_item); insert_item.connect(do_insert_item);
remove_item.connect(on_remove_item); remove_item.connect(do_remove_item);
Application app = GLib.Application.get_default() as Application; Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
@ -82,49 +84,57 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
} }
this.conversation = conversation; this.conversation = conversation;
stack.set_visible_child_name("void"); stack.set_visible_child_name("void");
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK);
}
content_populator.init(this, conversation, Plugins.WidgetType.GTK);
subscription_notification.init(conversation, this);
display_latest();
stack.set_visible_child_name("main");
}
private void display_latest() {
clear(); clear();
was_upper = null; was_upper = null;
was_page_size = null; was_page_size = null;
animate = false; animate = false;
Timeout.add(20, () => { animate = true; return false; }); Timeout.add(20, () => { animate = true; return false; });
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK);
}
content_populator.init(this, conversation, Plugins.WidgetType.GTK);
Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40); Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40);
foreach (ContentMetaItem item in items) { foreach (ContentMetaItem item in items) {
on_insert_item(item); do_insert_item(item);
} }
Idle.add(() => { on_value_notify(); return false; }); Idle.add(() => { on_value_notify(); return false; });
subscription_notification.init(conversation, this);
stack.set_visible_child_name("main");
} }
public void on_insert_item(Plugins.MetaConversationItem item) { public void do_insert_item(Plugins.MetaConversationItem item) {
lock (meta_items) { lock (meta_items) {
if (!item.can_merge || !merge_back(item)) { if (!item.can_merge || !merge_back(item)) {
insert_new(item); insert_new(item);
} }
} }
if (item as ContentMetaItem != null) {
content_items.add(item);
}
meta_items.add(item);
} }
private void on_remove_item(Plugins.MetaConversationItem item) { private void do_remove_item(Plugins.MetaConversationItem item) {
lock (meta_items) { ConversationItemSkeleton? skeleton = item_item_skeletons[item];
ConversationItemSkeleton? skeleton = item_item_skeletons[item]; if (skeleton.items.size > 1) {
if (skeleton.items.size > 1) { skeleton.remove_meta_item(item);
skeleton.remove_meta_item(item); } else {
} else { widgets[item].destroy();
widgets[item].destroy(); widgets.unset(item);
widgets.unset(item); skeleton.destroy();
skeleton.destroy(); item_skeletons.remove(skeleton);
item_skeletons.remove(skeleton); item_item_skeletons.unset(item);
item_item_skeletons.unset(item);
}
meta_items.remove(item);
} }
content_items.remove(item);
meta_items.remove(item);
} }
public void add_notification(Widget widget) { public void add_notification(Widget widget) {
@ -154,8 +164,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
lower_skeleton.add_meta_item(item); lower_skeleton.add_meta_item(item);
force_alloc_width(lower_skeleton, main.get_allocated_width()); force_alloc_width(lower_skeleton, main.get_allocated_width());
widgets[item] = widgets[lower_start_item];
item_item_skeletons[item] = lower_skeleton; item_item_skeletons[item] = lower_skeleton;
meta_items.add(item);
return true; return true;
} }
@ -182,7 +192,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
item_item_skeletons[item] = item_skeleton; item_item_skeletons[item] = item_skeleton;
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
item_skeletons.insert(index, item_skeleton); item_skeletons.insert(index, item_skeleton);
meta_items.add(item);
// Insert widget // Insert widget
Widget insert = item_skeleton; Widget insert = item_skeleton;
@ -220,12 +229,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
while(i < split_skeleton.items.size) { while(i < split_skeleton.items.size) {
Plugins.MetaConversationItem meta_item = split_skeleton.items[i]; Plugins.MetaConversationItem meta_item = split_skeleton.items[i];
if (time.compare(meta_item.display_time) < 0) { if (time.compare(meta_item.display_time) < 0) {
remove_item(meta_item); do_remove_item(meta_item);
if (!already_divided) { if (!already_divided) {
insert_new(meta_item); insert_new(meta_item);
already_divided = true; already_divided = true;
} else { } else {
insert_item(meta_item); do_insert_item(meta_item);
} }
} }
i++; i++;
@ -235,19 +244,24 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
private void on_upper_notify() { private void on_upper_notify() {
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 || 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 > 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 if (at_current_content) {
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) { } 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 scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
} }
was_upper = scrolled.vadjustment.upper; was_upper = scrolled.vadjustment.upper;
was_page_size = scrolled.vadjustment.page_size; was_page_size = scrolled.vadjustment.page_size;
was_value = scrolled.vadjustment.value;
reloading_mutex.trylock(); reloading_mutex.trylock();
reloading_mutex.unlock(); reloading_mutex.unlock();
} }
private void on_value_notify() { private void on_value_notify() {
if (scrolled.vadjustment.value < 200) { if (scrolled.vadjustment.value < 400) {
load_earlier_messages(); load_earlier_messages();
} else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
load_later_messages();
} }
} }
@ -255,14 +269,39 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
was_value = scrolled.vadjustment.value; was_value = scrolled.vadjustment.value;
if (!reloading_mutex.trylock()) return; if (!reloading_mutex.trylock()) return;
if (meta_items.size > 0) { if (meta_items.size > 0) {
Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, meta_items.first(), 20); Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, content_items.first(), 20);
foreach (ContentMetaItem item in items) { foreach (ContentMetaItem item in items) {
on_insert_item(item); do_insert_item(item);
} }
} else {
reloading_mutex.unlock();
} }
} }
private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { private void load_later_messages() {
if (!reloading_mutex.trylock()) return;
if (meta_items.size > 0 && !at_current_content) {
foreach (Plugins.MetaConversationItem a in content_items) {
ContentMetaItem b = a as ContentMetaItem;
MessageItem c = b.content_item as MessageItem;
}
Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, content_items.last(), 20);
ContentMetaItem b = content_items.last() as ContentMetaItem;
MessageItem c = b.content_item as MessageItem;
if (items.size == 0) {
at_current_content = true;
}
foreach (ContentMetaItem item in items) {
do_insert_item(item);
}
} else {
reloading_mutex.unlock();
}
}
private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
int res = a.sort_time.compare(b.sort_time); int res = a.sort_time.compare(b.sort_time);
if (res == 0) { if (res == 0) {
if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1; if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1;
@ -281,6 +320,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
} }
private void clear() { private void clear() {
content_items.clear();
meta_items.clear(); meta_items.clear();
item_skeletons.clear(); item_skeletons.clear();
item_item_skeletons.clear(); item_item_skeletons.clear();