Skip to content

Commit

Permalink
Undo toast for deleted messages (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmhewitt authored and Corentin Noël committed Jan 10, 2019
1 parent e6aae00 commit 398f6ef
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 27 deletions.
77 changes: 77 additions & 0 deletions src/Backend/TrashHandler.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*-
* Copyright 2018-2019 elementary, Inc. (https://elementary.io)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Authored by: David Hewitt <davidmhewitt@gmail.com>
*/

public class Mail.TrashHandler {
private Camel.Folder previous_folder;
private Gee.ArrayList<weak Camel.MessageInfo> deleted_messages;
private bool frozen = false;
private uint timeout_id = 0;

public int delete_threads (Camel.Folder folder, Gee.ArrayList<Camel.FolderThreadNode?> threads) {
previous_folder = folder;

deleted_messages = new Gee.ArrayList<weak Camel.MessageInfo> ();

foreach (var thread in threads) {
collect_thread_messages (thread);
}

folder.freeze ();
frozen = true;

timeout_id = Timeout.add_seconds (10, () => {
expire_undo ();
timeout_id = 0;
return Source.REMOVE;
});

foreach (var info in deleted_messages) {
info.set_flags (Camel.MessageFlags.DELETED, ~0);
}

return deleted_messages.size;
}

public void undo_last_delete () {
foreach (var info in deleted_messages) {
info.set_flags (Camel.MessageFlags.DELETED, 0);
}
}

public void expire_undo () {
if (timeout_id != 0) {
Source.remove (timeout_id);
timeout_id = 0;
}

if (frozen) {
frozen = false;
previous_folder.thaw ();
}
}

private void collect_thread_messages (Camel.FolderThreadNode thread) {
deleted_messages.add (thread.message);
unowned Camel.FolderThreadNode? child = (Camel.FolderThreadNode?) thread.child;
while (child != null) {
collect_thread_messages (child);
child = (Camel.FolderThreadNode?) child.next;
}
}
}
6 changes: 6 additions & 0 deletions src/ConversationList/ConversationItemModel.vala
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public class Mail.ConversationItemModel : GLib.Object {
}
}

public bool deleted {
get {
return Camel.MessageFlags.DELETED in (int)node.message.flags;
}
}

public int64 timestamp {
get {
return get_newest_timestamp (node);
Expand Down
27 changes: 27 additions & 0 deletions src/ConversationList/ConversationListBox.vala
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ public class Mail.ConversationListBox : VirtualizingListBox {
private string current_folder;
private Gee.HashMap<string, ConversationItemModel> conversations;
private ConversationListStore list_store;
private TrashHandler trash_handler;

construct {
activate_on_single_click = true;
conversations = new Gee.HashMap<string, ConversationItemModel> ();
list_store = new ConversationListStore ();
list_store.set_sort_func (thread_sort_function);
list_store.set_filter_func ((obj) => {
return !((ConversationItemModel)obj).deleted;
});

model = list_store;
trash_handler = new TrashHandler ();

factory_func = (item, old_widget) => {
ConversationListItem? row = null;
Expand Down Expand Up @@ -156,4 +162,25 @@ public class Mail.ConversationListBox : VirtualizingListBox {
private static int thread_sort_function (ConversationItemModel item1, ConversationItemModel item2) {
return (int)(item2.timestamp - item1.timestamp);
}

public int trash_selected_messages () {
var threads = new Gee.ArrayList<Camel.FolderThreadNode?> ();
var selected_rows = get_selected_rows ();
foreach (var row in selected_rows) {
threads.add (((ConversationItemModel)row).node);
}

var deleted = trash_handler.delete_threads (folder, threads);
list_store.items_changed (0, 0, list_store.get_n_items ());
return deleted;
}

public void undo_trash () {
trash_handler.undo_last_delete ();
list_store.items_changed (0, 0, list_store.get_n_items ());
}

public void undo_expired () {
trash_handler.expire_undo ();
}
}
27 changes: 25 additions & 2 deletions src/ConversationList/ConversationListStore.vala
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,27 @@
*/

public class Mail.ConversationListStore : VirtualizingListBoxModel {
public delegate bool RowVisibilityFunc (GLib.Object row);

private GLib.Sequence<ConversationItemModel> data = new GLib.Sequence<ConversationItemModel> ();
private uint last_position = -1u;
private GLib.SequenceIter<ConversationItemModel>? last_iter;
private unowned GLib.CompareDataFunc<ConversationItemModel> compare_func;
private unowned RowVisibilityFunc filter_func;

public override uint get_n_items () {
return data.get_length ();
}

public override GLib.Object? get_item (uint index) {
return get_item_internal (index);
}

public override GLib.Object? get_item_unfiltered (uint index) {
return get_item_internal (index, true);
}

private GLib.Object? get_item_internal (uint index, bool unfiltered = false) {
GLib.SequenceIter<ConversationItemModel>? iter = null;

if (last_position != -1u) {
Expand All @@ -54,7 +65,15 @@ public class Mail.ConversationListStore : VirtualizingListBoxModel {
return null;
}

return iter.get ();
if (filter_func == null) {
return iter.get ();
} else if (filter_func (iter.get ())) {
return iter.get ();
} else if (unfiltered) {
return iter.get ();
} else {
return null;
}
}

public void add (ConversationItemModel data) {
Expand All @@ -69,7 +88,7 @@ public class Mail.ConversationListStore : VirtualizingListBoxModel {
}

public void remove (ConversationItemModel data) {
var iter = this.data.get_iter_at_pos (get_index_of (data));
var iter = this.data.get_iter_at_pos (get_index_of_unfiltered (data));
iter.remove ();

last_iter = null;
Expand All @@ -87,4 +106,8 @@ public class Mail.ConversationListStore : VirtualizingListBoxModel {
public void set_sort_func (GLib.CompareDataFunc<ConversationItemModel> function) {
this.compare_func = function;
}

public void set_filter_func (RowVisibilityFunc function) {
filter_func = function;
}
}
50 changes: 29 additions & 21 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public class Mail.MainWindow : Gtk.ApplicationWindow {
private Gtk.Paned paned_start;

private FoldersListView folders_list_view;
private Gtk.Overlay conversation_list_overlay;
private ConversationListBox conversation_list_box;
private Gtk.ScrolledWindow conversation_list_scrolled;
private MessageListBox message_list_box;
private Gtk.ScrolledWindow message_list_scrolled;

Expand Down Expand Up @@ -70,7 +72,7 @@ public class Mail.MainWindow : Gtk.ApplicationWindow {

foreach (var action in action_accelerators.get_keys ()) {
((Gtk.Application) GLib.Application.get_default ()).set_accels_for_action (
ACTION_PREFIX + action,
ACTION_PREFIX + action,
action_accelerators[action].to_array ()
);
}
Expand All @@ -87,11 +89,14 @@ public class Mail.MainWindow : Gtk.ApplicationWindow {
message_list_box.bind_property ("can-reply", get_action (ACTION_FORWARD), "enabled", BindingFlags.SYNC_CREATE);
message_list_box.bind_property ("can-move-thread", get_action (ACTION_MOVE_TO_TRASH), "enabled", BindingFlags.SYNC_CREATE);

var conversation_list_scrolled = new Gtk.ScrolledWindow (null, null);
conversation_list_scrolled = new Gtk.ScrolledWindow (null, null);
conversation_list_scrolled.hscrollbar_policy = Gtk.PolicyType.NEVER;
conversation_list_scrolled.width_request = 158;
conversation_list_scrolled.add (conversation_list_box);

conversation_list_overlay = new Gtk.Overlay ();
conversation_list_overlay.add (conversation_list_scrolled);

message_list_scrolled = new Gtk.ScrolledWindow (null, null);
message_list_scrolled.hscrollbar_policy = Gtk.PolicyType.NEVER;
message_list_scrolled.add (message_list_box);
Expand All @@ -118,7 +123,7 @@ public class Mail.MainWindow : Gtk.ApplicationWindow {

paned_start = new Gtk.Paned (Gtk.Orientation.HORIZONTAL);
paned_start.pack1 (folders_list_view, false, false);
paned_start.pack2 (conversation_list_scrolled, true, false);
paned_start.pack2 (conversation_list_overlay, true, false);

paned_end = new Gtk.Paned (Gtk.Orientation.HORIZONTAL);
paned_end.pack1 (paned_start, false, false);
Expand Down Expand Up @@ -199,27 +204,30 @@ public class Mail.MainWindow : Gtk.ApplicationWindow {
}

private void on_move_to_trash () {
try {
var account = conversation_list_box.current_account;
var offline_store = (Camel.OfflineStore) account.service;
var trash_folder = offline_store.get_trash_folder_sync ();
if (trash_folder == null) {
critical ("Could not find trash folder in account " + account.service.display_name);
var result = conversation_list_box.trash_selected_messages ();
if (result > 0) {
foreach (weak Gtk.Widget child in conversation_list_overlay.get_children ()) {
if (child != conversation_list_scrolled) {
child.destroy ();
}
}

var source_folder = conversation_list_box.folder;
var uids = message_list_box.uids;
var toast = new Granite.Widgets.Toast (ngettext("Message Deleted", "Messages Deleted", result));
toast.set_default_action (_("Undo"));
toast.show_all ();

trash_folder.freeze ();
source_folder.freeze ();
try {
source_folder.transfer_messages_to_sync (uids, trash_folder, true, null);
} finally {
trash_folder.thaw ();
source_folder.thaw ();
}
} catch (Error e) {
critical ("Could not move messages to trash: " + e.message);
toast.default_action.connect (() => {
conversation_list_box.undo_trash ();
});

toast.notify["child-revealed"].connect (() => {
if (!toast.child_revealed) {
conversation_list_box.undo_expired ();
}
});

conversation_list_overlay.add_overlay (toast);
toast.send_notification ();
}
}

Expand Down
18 changes: 17 additions & 1 deletion src/VirtualizingListBox/VirtualizingListBox.vala
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,11 @@ public class VirtualizingListBox : Gtk.Container, Gtk.Scrollable {
return typeof (VirtualizingListBoxRow);
}

private VirtualizingListBoxRow get_widget (uint index) {
private VirtualizingListBoxRow? get_widget (uint index) {
var item = model.get_object (index);
if (item == null) {
return null;
}

VirtualizingListBoxRow? old_widget = null;
if (recycled_widgets.size > 0) {
Expand Down Expand Up @@ -361,6 +364,10 @@ public class VirtualizingListBox : Gtk.Container, Gtk.Scrollable {
while (shown_from > 0 && bin_y >= 0) {
shown_from--;
var new_widget = get_widget (shown_from);
if (new_widget == null) {
continue;
}

insert_child_internal (new_widget, 0);
var min = get_widget_height (new_widget);

Expand Down Expand Up @@ -401,6 +408,11 @@ public class VirtualizingListBox : Gtk.Container, Gtk.Scrollable {
bool added = false;
while (bin_y + bin_height <= get_allocated_height () && shown_to < model.get_n_items ()) {
var new_widget = get_widget (shown_to);
if (new_widget == null) {
shown_to++;
continue;
}

insert_child_internal (new_widget, current_widgets.size);

int min = get_widget_height (new_widget);
Expand Down Expand Up @@ -827,4 +839,8 @@ public class VirtualizingListBox : Gtk.Container, Gtk.Scrollable {
border = Gtk.Border ();
return false;
}

public Gee.HashSet<weak GLib.Object> get_selected_rows () {
return model.get_selected_rows ();
}
}
Loading

0 comments on commit 398f6ef

Please sign in to comment.