Commit d4676471 authored by Michael Gratton's avatar Michael Gratton 🤞

Break out ListBox used to display conversations into standalone widget.

The conversation viewer's ListBox is sufficiently complex to warrant its
own widget. Use empty placeholders for the list per the HIG, and
correctly fix mamagement of empty folder vs no conversations selected
this time.

* src/client/application/geary-controller.vala (GearyController):
  Directly manage secondary parts of the conversation viewer, since the
  controller since it has a better and more timely idea of when a
  conversation change is due to folder loading status or from the user
  selecting conversations, and so the viwer doesn't need to hook back
  into the controller. Remove the now-unused conversations_selected
  signal and its callers.

* src/client/conversation-viewer/conversation-listbox.vala: New widget
  for displaying the list of emails for a conversation. Moved relevant
  code from ConversationViewer here. Made adding emails async to get
  better UI responsiveness. Don't implement anything to handle
  conversation changes or emptying the list.

* src/client/conversation-viewer/conversation-viewer.vala: Replace user
  messages - empty folder/search & no/multiple messages selected with new
  EmptyPlaceholder. Remove a lot of the state manage code needed when
  managing the email listbox. Add a new ConversationListBox for every new
  conversation and just throw away.

* src/client/conversation-list/conversation-list-view.vala
  (ConversationListView): Clean up firing the conversations_selected
  signal - don't actually emit it when the model is clearing, and don't
  bother delaying the check either.

* src/client/components/empty-placeholder.vala: New widget for displaying
  empty list and grid placeholders per the HIG.

* src/client/conversation-viewer/conversation-email.vala
  (ConversationEmail): Make manually read a property, since it
  effectively is one.

* src/CMakeLists.txt: Include new source files.

* po/POTFILES.in: Include new source and UI files, and some missing ones.

* ui/CMakeLists.txt: Include new UI files.

* ui/conversation-viewer.ui: Replace user message and splash page with
  placeholders for the new empty placeholders(!).

* ui/empty-placeholder.ui: UI def for new widget class.

* ui/geary.css: Chase widget name/class changes, style new
  empty placeholder UI.
parent 9f185454
......@@ -24,6 +24,7 @@ src/client/application/main.vala
src/client/application/secret-mediator.vala
src/client/components/conversation-find-bar.vala
src/client/components/count-badge.vala
src/client/components/empty-placeholder.vala
src/client/components/folder-popover.vala
src/client/components/icon-factory.vala
src/client/components/main-toolbar.vala
......@@ -51,8 +52,10 @@ src/client/conversation-list/conversation-list-cell-renderer.vala
src/client/conversation-list/conversation-list-store.vala
src/client/conversation-list/conversation-list-view.vala
src/client/conversation-list/formatted-conversation-data.vala
src/client/conversation-viewer/conversation-viewer.vala
src/client/conversation-viewer/conversation-email.vala
src/client/conversation-viewer/conversation-listbox.vala
src/client/conversation-viewer/conversation-message.vala
src/client/conversation-viewer/conversation-viewer.vala
src/client/conversation-viewer/conversation-web-view.vala
src/client/dialogs/alert-dialog.vala
src/client/dialogs/attachment-dialog.vala
......
......@@ -333,6 +333,7 @@ client/accounts/login-dialog.vala
client/components/conversation-find-bar.vala
client/components/count-badge.vala
client/components/empty-placeholder.vala
client/components/folder-popover.vala
client/components/icon-factory.vala
client/components/main-toolbar.vala
......@@ -363,6 +364,7 @@ client/conversation-list/conversation-list-view.vala
client/conversation-list/formatted-conversation-data.vala
client/conversation-viewer/conversation-email.vala
client/conversation-viewer/conversation-listbox.vala
client/conversation-viewer/conversation-message.vala
client/conversation-viewer/conversation-viewer.vala
client/conversation-viewer/conversation-web-view.vala
......
......@@ -142,12 +142,6 @@ public class GearyController : Geary.BaseObject {
*/
public signal void folder_selected(Geary.Folder? folder);
/**
* Fired when the currently selected conversation(s) has/have changed.
*/
public signal void conversations_selected(Gee.Set<Geary.App.Conversation> conversations,
Geary.Folder current_folder);
/**
* Fired when the number of conversations changes.
*/
......@@ -228,12 +222,15 @@ public class GearyController : Geary.BaseObject {
main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
main_window.search_bar.search_text_changed.connect(on_search_text_changed);
main_window.conversation_viewer.email_row_added.connect(on_email_row_added);
main_window.conversation_viewer.email_row_removed.connect(on_email_row_removed);
main_window.conversation_viewer.mark_emails.connect(on_conversation_viewer_mark_emails);
main_window.conversation_viewer.conversation_added.connect(
on_conversation_view_added
);
main_window.conversation_viewer.conversation_removed.connect(
on_conversation_view_removed
);
new_messages_monitor = new NewMessagesMonitor(should_notify_new_messages);
main_window.folder_list.set_new_messages_monitor(new_messages_monitor);
// New messages indicator (Ubuntuism)
new_messages_indicator = NewMessagesIndicator.create(new_messages_monitor);
new_messages_indicator.application_activated.connect(on_indicator_activated_application);
......@@ -289,8 +286,8 @@ public class GearyController : Geary.BaseObject {
Geary.Engine.instance.account_available.disconnect(on_account_available);
Geary.Engine.instance.account_unavailable.disconnect(on_account_unavailable);
Geary.Engine.instance.untrusted_host.disconnect(on_untrusted_host);
// Connect to various UI signals.
// Disconnect from various UI signals.
main_window.conversation_list_view.conversations_selected.disconnect(on_conversations_selected);
main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated);
main_window.conversation_list_view.load_more.disconnect(on_load_more);
......@@ -302,9 +299,12 @@ public class GearyController : Geary.BaseObject {
main_window.main_toolbar.copy_folder_menu.folder_selected.disconnect(on_copy_conversation);
main_window.main_toolbar.move_folder_menu.folder_selected.disconnect(on_move_conversation);
main_window.search_bar.search_text_changed.disconnect(on_search_text_changed);
main_window.conversation_viewer.email_row_added.disconnect(on_email_row_added);
main_window.conversation_viewer.email_row_removed.disconnect(on_email_row_removed);
main_window.conversation_viewer.mark_emails.disconnect(on_conversation_viewer_mark_emails);
main_window.conversation_viewer.conversation_added.disconnect(
on_conversation_view_added
);
main_window.conversation_viewer.conversation_removed.disconnect(
on_conversation_view_removed
);
// hide window while shutting down, as this can take a few seconds under certain conditions
main_window.hide();
......@@ -1297,14 +1297,15 @@ public class GearyController : Geary.BaseObject {
private void on_folder_selected(Geary.Folder? folder) {
debug("Folder %s selected", folder != null ? folder.to_string() : "(null)");
this.main_window.conversation_viewer.show_loading();
// If the folder is being unset, clear the message list and exit here.
if (folder == null) {
current_folder = null;
main_window.conversation_list_store.clear();
main_window.main_toolbar.folder = null;
folder_selected(null);
return;
}
......@@ -1444,12 +1445,22 @@ public class GearyController : Geary.BaseObject {
on_load_more();
}
}
private void on_conversation_count_changed() {
if (current_conversations != null)
conversation_count_changed(current_conversations.get_conversation_count());
if (this.current_conversations != null) {
int count = this.current_conversations.get_conversation_count();
if (count == 0) {
// Let the user know if there's no available conversations
if (this.current_folder is Geary.SearchFolder) {
this.main_window.conversation_viewer.show_empty_search();
} else {
this.main_window.conversation_viewer.show_empty_folder();
}
}
conversation_count_changed(count);
}
}
private void on_libnotify_invoked(Geary.Folder? folder, Geary.Email? email) {
new_messages_monitor.clear_all_new_messages();
......@@ -1491,11 +1502,13 @@ public class GearyController : Geary.BaseObject {
debug("Unable to select folder: %s", err.message);
}
}
private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
selected_conversations = selected;
if (current_folder != null) {
conversations_selected(selected_conversations, current_folder);
if (this.current_folder != null) {
this.main_window.conversation_viewer.load_conversations.begin(
selected, this.current_folder
);
}
}
......@@ -1811,9 +1824,13 @@ public class GearyController : Geary.BaseObject {
Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
mark_email(ids, null, flags);
foreach (Geary.EmailIdentifier id in ids)
main_window.conversation_viewer.mark_manual_read(id);
ConversationListBox? list =
main_window.conversation_viewer.current_list;
if (list != null) {
foreach (Geary.EmailIdentifier id in ids)
list.mark_manual_read(id);
}
}
private void on_mark_as_unread() {
......@@ -1822,9 +1839,13 @@ public class GearyController : Geary.BaseObject {
Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(true);
mark_email(ids, flags, null);
foreach (Geary.EmailIdentifier id in ids)
main_window.conversation_viewer.mark_manual_read(id);
ConversationListBox? list =
main_window.conversation_viewer.current_list;
if (list != null) {
foreach (Geary.EmailIdentifier id in ids)
list.mark_manual_unread(id);
}
}
private void on_mark_as_starred() {
......@@ -2142,15 +2163,19 @@ public class GearyController : Geary.BaseObject {
// was triggered. If null, this was triggered from the headerbar
// or shortcut.
private void create_reply_forward_widget(ComposerWidget.ComposeType compose_type,
owned ConversationEmail? view) {
if (view == null) {
view = main_window.conversation_viewer.get_reply_email_view();
owned ConversationEmail? email_view) {
if (email_view == null) {
ConversationListBox? list_view =
main_window.conversation_viewer.current_list;
if (list_view != null) {
email_view = list_view.reply_target;
}
}
string? quote = null;
if (view != null) {
quote = view.get_body_selection();
if (email_view != null) {
quote = email_view.get_body_selection();
}
create_compose_widget(compose_type, view.email, quote);
create_compose_widget(compose_type, email_view.email, quote);
}
private void create_compose_widget(ComposerWidget.ComposeType compose_type,
......@@ -2177,7 +2202,10 @@ public class GearyController : Geary.BaseObject {
bool inline;
if (!should_create_new_composer(compose_type, referred, quote, is_draft, out inline))
return;
ConversationListBox? conversation_view =
main_window.conversation_viewer.current_list;
ComposerWidget widget;
if (mailto != null) {
widget = new ComposerWidget.from_mailto(current_account, mailto);
......@@ -2196,7 +2224,9 @@ public class GearyController : Geary.BaseObject {
widget = new ComposerWidget(current_account, compose_type, full, quote, is_draft);
if (is_draft) {
yield widget.restore_draft_state_async(current_account);
main_window.conversation_viewer.blacklist_by_id(referred.id);
if (conversation_view != null) {
conversation_view.blacklist_by_id(referred.id);
}
}
}
widget.show_all();
......@@ -2212,7 +2242,14 @@ public class GearyController : Geary.BaseObject {
widget.state == ComposerWidget.ComposerState.PANED) {
main_window.conversation_viewer.do_compose(widget);
} else {
main_window.conversation_viewer.do_embedded_composer(widget, referred);
ComposerEmbed embed = new ComposerEmbed(
referred,
widget,
main_window.conversation_viewer.conversation_page
);
if (conversation_view != null) {
conversation_view.add_embedded_composer(embed);
}
}
} else {
new ComposerWindow(widget);
......@@ -2471,13 +2508,15 @@ public class GearyController : Geary.BaseObject {
Cancellable? cancellable) throws Error {
if (!can_switch_conversation_view())
return;
if (main_window.conversation_viewer.current_conversation != null
&& main_window.conversation_viewer.current_conversation == last_deleted_conversation) {
ConversationListBox list_view =
main_window.conversation_viewer.current_list;
if (list_view != null &&
list_view.conversation == last_deleted_conversation) {
debug("Not archiving/trashing/deleting; viewed conversation is last deleted conversation");
return;
}
last_deleted_conversation = selected_conversations.size > 0
? Geary.traverse<Geary.App.Conversation>(selected_conversations).first() : null;
......@@ -2606,15 +2645,27 @@ public class GearyController : Geary.BaseObject {
}
private void on_zoom_in() {
this.main_window.conversation_viewer.zoom_in();
ConversationListBox? view =
main_window.conversation_viewer.current_list;
if (view != null) {
view.zoom_in();
}
}
private void on_zoom_out() {
this.main_window.conversation_viewer.zoom_out();
ConversationListBox? view =
main_window.conversation_viewer.current_list;
if (view != null) {
view.zoom_out();
}
}
private void on_zoom_normal() {
this.main_window.conversation_viewer.zoom_reset();
ConversationListBox? view =
main_window.conversation_viewer.current_list;
if (view != null) {
view.zoom_reset();
}
}
private void on_search() {
......@@ -2629,28 +2680,40 @@ public class GearyController : Geary.BaseObject {
Libnotify.play_sound("message-sent-email");
}
private void on_email_row_added(ConversationEmail message) {
message.reply_to_message.connect(on_reply_to_message);
message.reply_all_message.connect(on_reply_all_message);
message.forward_message.connect(on_forward_message);
message.link_activated.connect(on_link_activated);
message.attachments_activated.connect(on_attachments_activated);
message.save_attachments.connect(on_save_attachments);
message.edit_draft.connect(on_edit_draft);
message.view_source.connect(on_view_source);
message.save_image.connect(on_save_buffer_to_file);
}
private void on_email_row_removed(ConversationEmail message) {
message.reply_to_message.disconnect(on_reply_to_message);
message.reply_all_message.disconnect(on_reply_all_message);
message.forward_message.disconnect(on_forward_message);
message.link_activated.disconnect(on_link_activated);
message.attachments_activated.disconnect(on_attachments_activated);
message.save_attachments.disconnect(on_save_attachments);
message.edit_draft.disconnect(on_edit_draft);
message.view_source.disconnect(on_view_source);
message.save_image.disconnect(on_save_buffer_to_file);
private void on_conversation_view_added(ConversationListBox list) {
list.email_added.connect(on_conversation_viewer_email_added);
list.email_removed.connect(on_conversation_viewer_email_removed);
list.mark_emails.connect(on_conversation_viewer_mark_emails);
}
private void on_conversation_view_removed(ConversationListBox list) {
list.email_added.disconnect(on_conversation_viewer_email_added);
list.email_removed.disconnect(on_conversation_viewer_email_removed);
list.mark_emails.disconnect(on_conversation_viewer_mark_emails);
}
private void on_conversation_viewer_email_added(ConversationEmail view) {
view.reply_to_message.connect(on_reply_to_message);
view.reply_all_message.connect(on_reply_all_message);
view.forward_message.connect(on_forward_message);
view.link_activated.connect(on_link_activated);
view.attachments_activated.connect(on_attachments_activated);
view.save_attachments.connect(on_save_attachments);
view.edit_draft.connect(on_edit_draft);
view.view_source.connect(on_view_source);
view.save_image.connect(on_save_buffer_to_file);
}
private void on_conversation_viewer_email_removed(ConversationEmail view) {
view.reply_to_message.disconnect(on_reply_to_message);
view.reply_all_message.disconnect(on_reply_all_message);
view.forward_message.disconnect(on_forward_message);
view.link_activated.disconnect(on_link_activated);
view.attachments_activated.disconnect(on_attachments_activated);
view.save_attachments.disconnect(on_save_attachments);
view.edit_draft.disconnect(on_edit_draft);
view.view_source.disconnect(on_view_source);
view.save_image.disconnect(on_save_buffer_to_file);
}
private void on_link_activated(string link) {
......
/*
* Copyright 2016 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A placeholder image and message for empty views.
*/
[GtkTemplate (ui = "/org/gnome/Geary/empty-placeholder.ui")]
public class EmptyPlaceholder : Gtk.Grid {
public string image_name {
owned get { return this.placeholder_image.icon_name; }
set { this.placeholder_image.icon_name = value; }
}
public string title {
get { return this.title_label.get_text(); }
set { this.title_label.set_text(value); }
}
public string subtitle {
get { return this.subtitle_label.get_text(); }
set { this.subtitle_label.set_text(value); }
}
[GtkChild]
private Gtk.Image placeholder_image;
[GtkChild]
private Gtk.Label title_label;
[GtkChild]
private Gtk.Label subtitle_label;
}
......@@ -10,13 +10,14 @@
*/
public class ComposerBox : Gtk.Frame, ComposerContainer {
public Gtk.ApplicationWindow top_window {
get { return (Gtk.ApplicationWindow) get_toplevel(); }
}
protected ComposerWidget composer { get; set; }
protected Gee.MultiMap<string, string>? old_accelerators { get; set; }
public Gtk.ApplicationWindow top_window {
get { return (Gtk.ApplicationWindow) get_toplevel(); }
}
public signal void vanished();
......
......@@ -555,9 +555,10 @@ public class ComposerWidget : Gtk.EventBox {
// from being finalised when closed.
ConversationViewer conversation_viewer =
GearyApplication.instance.controller.main_window.conversation_viewer;
conversation_viewer.cleared.connect((viewer) => {
if (this.draft_manager != null)
viewer.blacklist_by_id(this.draft_manager.current_draft_id);
conversation_viewer.conversation_added.connect((list_view) => {
if (this.draft_manager != null) {
list_view.blacklist_by_id(this.draft_manager.current_draft_id);
}
});
// Don't do this in an overridden version of the destroy
......@@ -1413,8 +1414,11 @@ public class ComposerWidget : Gtk.EventBox {
}
private void on_draft_id_changed() {
GearyApplication.instance.controller.main_window.conversation_viewer.blacklist_by_id(
this.draft_manager.current_draft_id);
ConversationListBox? list_view =
GearyApplication.instance.controller.main_window.conversation_viewer.current_list;
if (list_view != null) {
list_view.blacklist_by_id(this.draft_manager.current_draft_id);
}
}
private void on_draft_manager_fatal(Error err) {
......@@ -1514,11 +1518,12 @@ public class ComposerWidget : Gtk.EventBox {
} catch (Error err) {
// ignored
}
if (this.draft_manager != null)
GearyApplication.instance.controller.main_window.conversation_viewer
.unblacklist_by_id(this.draft_manager.current_draft_id);
this.container.close_container();
ConversationListBox? list_view =
GearyApplication.instance.controller.main_window.conversation_viewer.current_list;
if (this.draft_manager != null && list_view != null) {
list_view.unblacklist_by_id(this.draft_manager.current_draft_id);
}
container.close_container();
}
private async void discard_and_exit_async() {
......
......@@ -311,69 +311,66 @@ public class ConversationListView : Gtk.TreeView {
private Gtk.TreePath? get_selected_path() {
return get_all_selected_paths().nth_data(0);
}
private void on_selection_changed() {
if (selection_changed_id != 0)
Source.remove(selection_changed_id);
// Schedule processing selection changes at low idle for two reasons: (a) if a lot of
// changes come in back-to-back, this allows for all that activity to settle before
// updating state and firing signals (which results in a lot of I/O), and (b) it means
// the ConversationMonitor's signals may be processed in any order by this class and the
// ConversationListView and not result in a lot of screen flashing and (again) unnecessary
// I/O as both classes update selection state.
selection_changed_id = Idle.add(() => {
// no longer scheduled
selection_changed_id = 0;
do_selection_changed();
return false;
}, Priority.LOW);
if (this.selection_changed_id != 0)
Source.remove(this.selection_changed_id);
if (this.conversation_list_store.is_clearing) {
// The list store is clearing, so the folder has changed
// and we don't want to notify about the selection
// changing, so just clear it.
this.selected.clear();
} else {
// Schedule processing selection changes at low idle for
// two reasons: (a) if a lot of changes come in
// back-to-back, this allows for all that activity to
// settle before updating state and firing signals (which
// results in a lot of I/O), and (b) it means the
// ConversationMonitor's signals may be processed in any
// order by this class and the ConversationListView and
// not result in a lot of screen flashing and (again)
// unnecessary I/O as both classes update selection state.
this.selection_changed_id = Idle.add(() => {
// no longer scheduled
this.selection_changed_id = 0;
// Pass the is_clearing flag through here so the value is
// accurate later on, when the idle callback actually
// happens.
do_selection_changed();
return false;
}, Priority.LOW);
}
}
// Gtk.TreeSelection can fire its "changed" signal even when nothing's changed, so look for that
// to avoid subscribers from doing the same things (in particular, I/O) multiple times
// Gtk.TreeSelection can fire its "changed" signal even when
// nothing's changed, so look for that to avoid subscribers from
// doing the same things (in particular, I/O) multiple times
private void do_selection_changed() {
// if the ConversationListStore is clearing, then this is called repeatedly as the elements
// are removed, causing signals to fire and a flurry of I/O that is immediately cancelled
// this prevents that, merely firing the signal once to indicate all selections are
// dropped while clearing
if (conversation_list_store.is_clearing) {
if (selected.size > 0) {
selected.clear();
conversations_selected(selected.read_only_view);
}
return;
}
Gee.HashSet<Geary.App.Conversation> new_selection =
new Gee.HashSet<Geary.App.Conversation>();
List<Gtk.TreePath> paths = get_all_selected_paths();
if (paths.length() == 0) {
// only notify if this is different than what was previously reported
if (selected.size != 0) {
selected.clear();
conversations_selected(selected.read_only_view);
if (paths.length() != 0) {
// Conversations are selected, so collect them and
// signal if different
foreach (Gtk.TreePath path in paths) {
Geary.App.Conversation? conversation =
this.conversation_list_store.get_conversation_at_path(path);
if (conversation != null)
new_selection.add(conversation);
}
return;
}
// Conversations are selected, so collect them and signal if different
Gee.HashSet<Geary.App.Conversation> new_selected = new Gee.HashSet<Geary.App.Conversation>();
foreach (Gtk.TreePath path in paths) {
Geary.App.Conversation? conversation = conversation_list_store.get_conversation_at_path(path);
if (conversation != null)
new_selected.add(conversation);
}
// only notify if different than what was previously reported
if (!Geary.Collection.are_sets_equal<Geary.App.Conversation>(selected, new_selected)) {
selected = new_selected;
conversations_selected(selected.read_only_view);
if (!Geary.Collection.are_sets_equal<Geary.App.Conversation>(
this.selected, new_selection)) {
this.selected = new_selection;
conversations_selected(this.selected.read_only_view);
}
}
public Gee.Set<Geary.App.Conversation> get_visible_conversations() {
Gee.HashSet<Geary.App.Conversation> visible_conversations = new Gee.HashSet<Geary.App.Conversation>();
......
......@@ -119,6 +119,7 @@ public class ConversationEmail : Gtk.Box {
private const string ACTION_UNSTAR = "unstar";
private const string ACTION_VIEW_SOURCE = "view_source";
private const string MANUAL_READ_CLASS = "geary-manual-read";
/** The specific email that is displayed by this view. */
public Geary.Email email { get; private set; }
......@@ -126,6 +127,18 @@ public class ConversationEmail : Gtk.Box {
/** Determines if the email is showing a preview or the full message. */
public bool is_collapsed = true;
/** Determines if the email has been manually marked as being read. */
public bool is_manually_read {
get { return get_style_context().has_class(MANUAL_READ_CLASS); }
set {
if (value) {
get_style_context().add_class(MANUAL_READ_CLASS);
} else {
get_style_context().remove_class(MANUAL_READ_CLASS);
}
}
}
/** The view displaying the email's primary message headers and body. */
public ConversationMessage primary_message { get; private set; }
......@@ -421,20 +434,6 @@ public class ConversationEmail : Gtk.Box {
update_email_state();
}
/**
* Determines if the email is flagged as read on the client side only.
*/
public bool is_manual_read() {
return get_style_context().has_class("geary_manual_read");
}
/**
* Displays the message as read, even if not reflected in its flags.
*/
public void mark_manual_read() {
get_style_context().add_class("geary_manual_read");
}
/**
* Returns user-selected body HTML from a message, if any.
*/
......
This diff is collapsed.
......@@ -15,6 +15,7 @@ set(RESOURCE_LIST
STRIPBLANKS "conversation-viewer.ui"
"conversation-web-view.css"
STRIPBLANKS "edit_alternate_emails.glade"
STRIPBLANKS "empty-placeholder.ui"
STRIPBLANKS "find_bar.glade"
STRIPBLANKS "folder-popover.ui"
STRIPBLANKS "gtk/help-overlay.ui"
......
......@@ -8,25 +8,26 @@
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkImage" id="splash_page">
<property name="name">splash_page</property>
<object class="GtkSpinner" id="loading_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">256</property>
<property name="icon_name">mail-inbox-symbolic</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">splash_page</property>
<property name="name">loading_page</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="loading_page">
<object class="GtkBox" id="no_conversations_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">loading_page</property>
<property name="name">no_conversations_page</property>
<property name="position">1</property>
</packing>
</child>
......@@ -34,26 +35,10 @@
<object class="GtkScrolledWindow" id="conversation_page">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<signal name="key-press-event" handler="on_conversation_key_press" swapped="no"/>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="conversation_listbox">
<property name="name">conversation_listbox</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<style>
<class name="background"/>
</style>
</object>
</child>