From 5cc92ef964cedcbd306feb04f3aae8ff88ce424e Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Wed, 30 Oct 2019 14:30:26 +1100 Subject: [PATCH] Move email action handling to ConversationListBox This allows a single widget to get constructed to handle email actions, rather than every single ConversationEmail having to do so, and thus related signals can also be moved to and emitted from ConversationListBox, so that MainWindow only has to hook up to a single object's signals for a conversation, not every email in the conversation. --- src/client/components/main-window.vala | 129 ++-- .../conversation-email.vala | 574 +++++++++--------- .../conversation-list-box.vala | 309 ++++++++-- src/client/util/util-gtk.vala | 77 +++ ui/conversation-email-menus.ui | 16 +- ui/conversation-email.ui | 8 +- 6 files changed, 663 insertions(+), 450 deletions(-) diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index 6f510205..5617f6a1 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -1390,27 +1390,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } private void on_conversation_view_added(ConversationListBox list) { - list.email_added.connect(on_conversation_viewer_email_added); - list.mark_emails.connect(on_mark_messages); - } - - private void on_conversation_viewer_email_added(ConversationEmail view) { - view.forward_message.connect(on_forward_message); - view.reply_all_message.connect(on_reply_all_message); - view.reply_to_message.connect(on_reply_to_message); - view.edit_draft.connect(on_edit_draft); - - Geary.App.Conversation conversation = this.conversation_viewer.current_list.conversation; - bool in_selected_folder = ( - conversation.is_in_base_folder(view.email.id) && - conversation.base_folder == selected_folder - ); - bool supports_trash = in_selected_folder && selected_folder_supports_trash(); - bool supports_delete = in_selected_folder && selected_folder is Geary.FolderSupport.Remove; - view.trash_message.connect(on_trash_message); - view.delete_message.connect(on_delete_message); - view.set_folder_actions_enabled(supports_trash, supports_delete); - this.on_shift_key.connect(view.shift_key_changed); + list.mark_email.connect(on_email_mark); + list.reply_to_all_email.connect(on_email_reply_to_all); + list.reply_to_sender_email.connect(on_email_reply_to_sender); + list.forward_email.connect(on_email_forward); + list.edit_email.connect(on_email_edit); + list.trash_email.connect(on_email_trash); + list.delete_email.connect(on_email_delete); } // Window-level action callbacks @@ -1768,20 +1754,30 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - // Individual message view action callbacks + // Individual conversation email view action callbacks - private void on_mark_messages(Geary.App.Conversation conversation, - Gee.Collection messages, - Geary.EmailFlags? to_add, - Geary.EmailFlags? to_remove) { + private void on_email_mark(ConversationListBox view, + Gee.Collection messages, + Geary.NamedFlag? to_add, + Geary.NamedFlag? to_remove) { Geary.Account? target = this.selected_account; if (target != null) { + Geary.EmailFlags add_flags = null; + if (to_add != null) { + add_flags = new Geary.EmailFlags(); + add_flags.add(to_add); + } + Geary.EmailFlags remove_flags = null; + if (to_remove != null) { + remove_flags = new Geary.EmailFlags(); + remove_flags.add(to_remove); + } this.application.controller.mark_messages.begin( target, - Geary.Collection.single(conversation), + Geary.Collection.single(view.conversation), messages, - to_add, - to_remove, + add_flags, + remove_flags, (obj, res) => { try { this.application.controller.mark_messages.end(res); @@ -1793,58 +1789,49 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - private void on_reply_to_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, REPLY, target_view.email, quote - ); - }); - } - } - - private void on_reply_all_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, REPLY_ALL, target_view.email, quote - ); - }); - } - } - - private void on_forward_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, FORWARD, target_view.email, quote - ); - }); - } - } - - private void on_edit_draft(ConversationEmail target_view) { + private void on_email_reply_to_sender(Geary.Email target, string? quote) { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, NEW_MESSAGE, target_view.email, null + account, REPLY, target, quote ); } } - private void on_trash_message(ConversationEmail target_view) { + private void on_email_reply_to_all(Geary.Email target, string? quote) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, REPLY_ALL, target, quote + ); + } + } + + private void on_email_forward(Geary.Email target, string? quote) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, FORWARD, target, quote + ); + } + } + + private void on_email_edit(Geary.Email target) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, NEW_MESSAGE, target, null + ); + } + } + + private void on_email_trash(Geary.Email target) { Geary.Folder? source = this.selected_folder; if (source != null) { this.application.controller.move_messages_special.begin( source, TRASH, - Geary.Collection.single(target_view.email.id), + Geary.Collection.single(target.id), (obj, res) => { try { this.application.controller.move_messages_special.end(res); @@ -1856,13 +1843,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - private void on_delete_message(ConversationEmail target_view) { + private void on_email_delete(Geary.Email target) { Geary.FolderSupport.Remove? source = this.selected_folder as Geary.FolderSupport.Remove; if (source != null && prompt_delete_messages(1)) { this.application.controller.delete_messages.begin( source, - Geary.Collection.single(target_view.email.id), + Geary.Collection.single(target.id), (obj, res) => { try { this.application.controller.delete_messages.end(res); diff --git a/src/client/conversation-viewer/conversation-email.vala b/src/client/conversation-viewer/conversation-email.vala index e77e6bcb..3e34b304 100644 --- a/src/client/conversation-viewer/conversation-email.vala +++ b/src/client/conversation-viewer/conversation-email.vala @@ -20,6 +20,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { // This isn't a Gtk.Grid since when added to a Gtk.ListBoxRow the // hover style isn't applied to it. + private const string MANUAL_READ_CLASS = "geary-manual-read"; + private const string SENT_CLASS = "geary-sent"; + private const string STARRED_CLASS = "geary-starred"; + private const string UNREAD_CLASS = "geary-unread"; /** Fields that must be available for constructing the view. */ internal const Geary.Email.Field REQUIRED_FOR_CONSTRUCT = ( @@ -127,24 +131,20 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } - private const string ACTION_FORWARD = "forward"; - private const string ACTION_MARK_READ = "mark_read"; - private const string ACTION_MARK_UNREAD = "mark_unread"; - private const string ACTION_MARK_UNREAD_DOWN = "mark_unread_down"; - private const string ACTION_TRASH_MESSAGE = "trash_msg"; - private const string ACTION_DELETE_MESSAGE = "delete_msg"; - private const string ACTION_PRINT = "print"; - private const string ACTION_REPLY_SENDER = "reply_sender"; - private const string ACTION_REPLY_ALL = "reply_all"; - private const string ACTION_SAVE_ALL_ATTACHMENTS = "save_all_attachments"; - private const string ACTION_STAR = "star"; - private const string ACTION_UNSTAR = "unstar"; - private const string ACTION_VIEW_SOURCE = "view_source"; + private static GLib.MenuModel email_menu_template; + private static GLib.MenuModel email_menu_trash_section; + private static GLib.MenuModel email_menu_delete_section; + + + static construct { + Gtk.Builder builder = new Gtk.Builder.from_resource( + "/org/gnome/Geary/conversation-email-menus.ui" + ); + email_menu_template = (GLib.MenuModel) builder.get_object("email_menu"); + email_menu_trash_section = (GLib.MenuModel) builder.get_object("email_menu_trash"); + email_menu_delete_section = (GLib.MenuModel) builder.get_object("email_menu_delete"); + } - private const string MANUAL_READ_CLASS = "geary-manual-read"; - private const string SENT_CLASS = "geary-sent"; - private const string STARRED_CLASS = "geary-starred"; - private const string UNREAD_CLASS = "geary-unread"; /** * The specific email that is displayed by this view. @@ -156,6 +156,22 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { */ public Geary.Email email { get; private set; } + /** Determines if this email currently flagged as unread. */ + public bool is_unread { + get { + Geary.EmailFlags? flags = this.email.email_flags; + return (flags != null && flags.is_unread()); + } + } + + /** Determines if this email currently flagged as starred. */ + public bool is_starred { + get { + Geary.EmailFlags? flags = this.email.email_flags; + return (flags != null && flags.is_flagged()); + } + } + /** Determines if the email is showing a preview or the full message. */ public bool is_collapsed = true; @@ -177,6 +193,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** The view displaying the email's primary message headers and body. */ public ConversationMessage primary_message { get; private set; } + public Components.AttachmentPane? attachments_pane { + get; private set; default = null; + } + /** Views for attached messages. */ public Gee.List attached_messages { owned get { return this._attached_messages.read_only_view; } @@ -187,6 +207,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** Determines the message body loading state. */ public LoadState message_body_state { get; private set; default = NOT_STARTED; } + public Geary.App.Conversation conversation; + // Store from which to load message content, if needed private Geary.App.EmailStore email_store; @@ -200,7 +222,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private Geary.TimeoutManager body_loading_timeout; - /** Determines if all message's web views have finished loading. */ private Geary.Nonblocking.Spinlock message_bodies_loaded_lock; @@ -212,8 +233,9 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private Gee.List displayed_attachments = new Gee.LinkedList(); - // Message-specific actions - private SimpleActionGroup message_actions = new SimpleActionGroup(); + // Tracks if Shift key handler has been installed on the main + // window, for updating email menu trash/delete actions. + private bool shift_handler_installed = false; [GtkChild] private Gtk.Grid actions; @@ -239,45 +261,9 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { [GtkChild] private Gtk.Grid sub_messages; - private Components.AttachmentPane? attachments_pane = null; - - private Menu email_menu; - private Menu email_menu_model; - private Menu email_menu_trash; - private Menu email_menu_delete; - private bool shift_key_down; - - - /** Fired when the user clicks "reply" in the message menu. */ - public signal void reply_to_message(); - - /** Fired when the user clicks "reply all" in the message menu. */ - public signal void reply_all_message(); - - /** Fired when the user clicks "forward" in the message menu. */ - public signal void forward_message(); - - /** Fired when the user updates the email's flags. */ - public signal void mark_email( - Geary.NamedFlag? to_add, Geary.NamedFlag? to_remove - ); - - /** Fired when the user updates flags for this email and all others down. */ - public signal void mark_email_from_here( - Geary.NamedFlag? to_add, Geary.NamedFlag? to_remove - ); - - /** Fired when the user clicks "trash" in the message menu. */ - public signal void trash_message(); - - /** Fired when the user clicks "delete" in the message menu. */ - public signal void delete_message(); - - /** Fired the edit draft button is clicked. */ - public signal void edit_draft(); /** Fired when a internal link is activated */ - public signal void internal_link_activated(int y); + internal signal void internal_link_activated(int y); /** Fired when the user selects text in a message. */ internal signal void body_selection_changed(bool has_selection); @@ -290,7 +276,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { * the complete email, but does not attempt any possibly * long-running loading processes. */ - public ConversationEmail(Geary.Email email, + public ConversationEmail(Geary.App.Conversation conversation, + Geary.Email email, Geary.App.EmailStore email_store, Application.ContactStore contacts, Configuration config, @@ -298,6 +285,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { bool is_draft, GLib.Cancellable load_cancellable) { base_ref(); + this.conversation = conversation; this.email = email; this.is_draft = is_draft; this.email_store = email_store; @@ -311,45 +299,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { get_style_context().add_class(SENT_CLASS); } - add_action(ACTION_FORWARD).activate.connect(() => { - forward_message(); - }); - add_action(ACTION_PRINT).activate.connect(() => { - print.begin(); - }); - add_action(ACTION_MARK_READ).activate.connect(() => { - mark_email(null, Geary.EmailFlags.UNREAD); - }); - add_action(ACTION_MARK_UNREAD).activate.connect(() => { - mark_email(Geary.EmailFlags.UNREAD, null); - }); - add_action(ACTION_MARK_UNREAD_DOWN).activate.connect(() => { - mark_email_from_here(Geary.EmailFlags.UNREAD, null); - }); - add_action(ACTION_TRASH_MESSAGE).activate.connect(() => { - trash_message(); - }); - add_action(ACTION_DELETE_MESSAGE).activate.connect(() => { - delete_message(); - }); - add_action(ACTION_REPLY_ALL).activate.connect(() => { - reply_all_message(); - }); - add_action(ACTION_REPLY_SENDER).activate.connect(() => { - reply_to_message(); - }); - add_action(ACTION_SAVE_ALL_ATTACHMENTS).activate.connect(() => { - this.attachments_pane.save_all(); - }); - add_action(ACTION_STAR).activate.connect(() => { - mark_email(Geary.EmailFlags.FLAGGED, null); - }); - add_action(ACTION_UNSTAR).activate.connect(() => { - mark_email(null, Geary.EmailFlags.FLAGGED); - }); - add_action(ACTION_VIEW_SOURCE).activate.connect(on_view_source); - insert_action_group("eml", message_actions); - // Construct the view for the primary message, hook into it this.primary_message = new ConversationMessage.from_email( @@ -361,30 +310,19 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { connect_message_view_signals(this.primary_message); this.primary_message.summary.add(this.actions); - - // Wire up the rest of the UI - - Gtk.Builder builder = new Gtk.Builder.from_resource( - "/org/gnome/Geary/conversation-email-menus.ui" - ); - this.email_menu = new Menu(); - this.email_menu_model = (Menu) builder.get_object("email_menu"); - this.email_menu_trash = (Menu) builder.get_object("email_menu_trash"); - this.email_menu_delete = (Menu) builder.get_object("email_menu_delete"); - this.email_menubutton.set_menu_model(this.email_menu); - this.email_menubutton.set_sensitive(false); - this.email_menubutton.toggled.connect(this.on_email_menu); - this.primary_message.infobars.add(this.draft_infobar); if (is_draft) { this.draft_infobar.show(); this.draft_infobar.response.connect((infobar, response_id) => { - if (response_id == 1) { edit_draft(); } + if (response_id == 1) { + activate_email_action(ConversationListBox.ACTION_EDIT); + } }); } - this.primary_message.infobars.add(this.not_saved_infobar); + // Wire up the rest of the UI + email_store.account.incoming.notify["current-status"].connect( on_service_status_change ); @@ -483,23 +421,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } } - /** - * Enables or disables actions that require folder support. - */ - public void set_folder_actions_enabled(bool supports_trash, bool supports_delete) { - set_action_enabled(ACTION_TRASH_MESSAGE, supports_trash); - set_action_enabled(ACTION_DELETE_MESSAGE, supports_delete); - } - - /** - * Substitutes the "Delete Message" button for the "Move Message to Trash" - * button if the Shift key is pressed. - */ - public void shift_key_changed(bool pressed) { - this.shift_key_down = pressed; - this.on_email_menu(); - } - /** * Shows the complete message: headers, body and attachments. */ @@ -507,7 +428,16 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.is_collapsed = false; update_email_state(); this.attachments_button.set_sensitive(true); - this.email_menubutton.set_sensitive(true); + // Needs at least some menu set otherwise it won't be enabled, + // also has the side effect of making it sensitive + this.email_menubutton.set_menu_model(new GLib.Menu()); + + // Set targets to enable the actions + GLib.Variant email_target = email.id.to_variant(); + this.attachments_button.set_action_target_value(email_target); + this.star_button.set_action_target_value(email_target); + this.unstar_button.set_action_target_value(email_target); + foreach (ConversationMessage message in this) { message.show_message_body(include_transitions); } @@ -521,6 +451,12 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { update_email_state(); attachments_button.set_sensitive(false); email_menubutton.set_sensitive(false); + + // Clear targets to disable the actions + this.attachments_button.set_action_target_value(null); + this.star_button.set_action_target_value(null); + this.unstar_button.set_action_target_value(null); + primary_message.hide_message_body(); foreach (ConversationMessage attached in this._attached_messages) { attached.hide_message_body(); @@ -567,6 +503,115 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return selection; } + /** Displays the raw RFC 822 source for this email. */ + public async void view_source() { + MainWindow? main = get_toplevel() as MainWindow; + if (main != null) { + Geary.Email email = this.email; + try { + yield Geary.Nonblocking.Concurrent.global.schedule_async( + () => { + string source = ( + email.header.buffer.to_string() + + email.body.buffer.to_string() + ); + string temporary_filename; + int temporary_handle = GLib.FileUtils.open_tmp( + "geary-message-XXXXXX.txt", + out temporary_filename + ); + GLib.FileUtils.set_contents(temporary_filename, source); + GLib.FileUtils.close(temporary_handle); + + // ensure this file is only readable by the + // user ... this needs to be done after the + // file is closed + GLib.FileUtils.chmod( + temporary_filename, + (int) (Posix.S_IRUSR | Posix.S_IWUSR) + ); + + string temporary_uri = GLib.Filename.to_uri( + temporary_filename, null + ); + main.application.show_uri.begin(temporary_uri); + }, + null + ); + } catch (GLib.Error error) { + main.application.controller.report_problem( + new Geary.ProblemReport(error) + ); + } + } + } + + /** Print this view's email. */ + public async void print() throws Error { + Json.Builder builder = new Json.Builder(); + builder.begin_object(); + if (this.email.from != null) { + builder.set_member_name(_("From:")); + builder.add_string_value(this.email.from.to_string()); + } + if (this.email.to != null) { + // Translators: Human-readable version of the RFC 822 To header + builder.set_member_name(_("To:")); + builder.add_string_value(this.email.to.to_string()); + } + if (this.email.cc != null) { + // Translators: Human-readable version of the RFC 822 CC header + builder.set_member_name(_("Cc:")); + builder.add_string_value(this.email.cc.to_string()); + } + if (this.email.bcc != null) { + // Translators: Human-readable version of the RFC 822 BCC header + builder.set_member_name(_("Bcc:")); + builder.add_string_value(this.email.bcc.to_string()); + } + if (this.email.date != null) { + // Translators: Human-readable version of the RFC 822 Date header + builder.set_member_name(_("Date:")); + builder.add_string_value( + Util.Date.pretty_print_verbose( + this.email.date.value.to_local(), + this.config.clock_format + ) + ); + } + if (this.email.subject != null) { + // Translators: Human-readable version of the RFC 822 Subject header + builder.set_member_name(_("Subject:")); + builder.add_string_value(this.email.subject.to_string()); + } + builder.end_object(); + Json.Generator generator = new Json.Generator(); + generator.set_root(builder.get_root()); + string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");"; + yield this.primary_message.web_view.run_javascript(js, null); + + Gtk.Window? window = get_toplevel() as Gtk.Window; + WebKit.PrintOperation op = new WebKit.PrintOperation( + this.primary_message.web_view + ); + Gtk.PrintSettings settings = new Gtk.PrintSettings(); + + if (this.email.subject != null) { + string file_name = Geary.String.reduce_whitespace(this.email.subject.value); + file_name = file_name.replace("/", "_"); + if (file_name.char_count() > 128) { + file_name = Geary.String.safe_byte_substring(file_name, 128); + } + + if (!Geary.String.is_empty(file_name)) { + settings.set(Gtk.PRINT_SETTINGS_OUTPUT_BASENAME, file_name); + } + } + + op.set_print_settings(settings); + op.run_dialog(window); + } + /** * Returns a new Iterable over all message views in this email view */ @@ -574,31 +619,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return new MessageViewIterator(this); } - private SimpleAction add_action(string name, bool enabled = true) { - SimpleAction action = new SimpleAction(name, null); - action.set_enabled(enabled); - message_actions.add_action(action); - return action; - } - - private bool get_action_enabled(string name) { - SimpleAction? action = - this.message_actions.lookup_action(name) as SimpleAction; - if (action != null) { - return action.get_enabled(); - } else { - return false; - } - } - - private void set_action_enabled(string name, bool enabled) { - SimpleAction? action = - this.message_actions.lookup_action(name) as SimpleAction; - if (action != null) { - action.set_enabled(enabled); - } - } - private void connect_message_view_signals(ConversationMessage view) { view.flag_remote_images.connect(on_flag_remote_images); view.internal_link_activated.connect((y) => { @@ -715,37 +735,103 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } private void update_email_state() { - Geary.EmailFlags? flags = this.email.email_flags; Gtk.StyleContext style = get_style_context(); - bool is_unread = (flags != null && flags.is_unread()); - set_action_enabled(ACTION_MARK_READ, is_unread); - set_action_enabled(ACTION_MARK_UNREAD, !is_unread); - set_action_enabled(ACTION_MARK_UNREAD_DOWN, !is_unread); - if (is_unread) { + if (this.is_unread) { style.add_class(UNREAD_CLASS); } else { style.remove_class(UNREAD_CLASS); } - bool is_flagged = (flags != null && flags.is_flagged()); - set_action_enabled(ACTION_STAR, !this.is_collapsed && !is_flagged); - set_action_enabled(ACTION_UNSTAR, !this.is_collapsed && is_flagged); - if (is_flagged) { + if (this.is_starred) { style.add_class(STARRED_CLASS); - star_button.hide(); - unstar_button.show(); + this.star_button.hide(); + this.unstar_button.show(); } else { style.remove_class(STARRED_CLASS); - star_button.show(); - unstar_button.hide(); + this.star_button.show(); + this.unstar_button.hide(); } - if (flags != null && flags.is_outbox_sent()) { + if (this.email.email_flags != null && + this.email.email_flags.is_outbox_sent()) { this.not_saved_infobar.show(); } + + update_email_menu(); } + private void update_email_menu() { + if (this.email_menubutton.active) { + bool in_base_folder = this.conversation.is_in_base_folder( + this.email.id + ); + bool supports_trash = ( + in_base_folder && + Application.Controller.does_folder_support_trash( + this.conversation.base_folder + ) + ); + bool supports_delete = ( + in_base_folder && + this.conversation.base_folder is Geary.FolderSupport.Remove + ); + bool is_shift_down = false; + MainWindow? main = get_toplevel() as MainWindow; + if (main != null) { + is_shift_down = main.is_shift_down; + + if (!this.shift_handler_installed) { + this.shift_handler_installed = true; + main.notify["is-shift-down"].connect(on_shift_changed); + } + } + + string[] blacklist = {}; + if (this.is_unread) { + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_UNREAD + ); + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_UNREAD_DOWN + ); + } else { + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_READ + ); + } + + bool show_trash = !is_shift_down && supports_trash; + bool show_delete = !show_trash && supports_delete; + GLib.Variant email_target = email.id.to_variant(); + GLib.Menu new_model = Util.Gtk.construct_menu( + email_menu_template, + (menu, submenu, action, item) => { + bool accept = true; + if (submenu == email_menu_trash_section && !show_trash) { + accept = false; + } + if (submenu == email_menu_delete_section && !show_delete) { + accept = false; + } + if (action != null && !(action in blacklist)) { + item.set_action_and_target_value( + action, email_target + ); + } + return accept; + } + ); + + this.email_menubutton.popover.bind_model(new_model, null); + this.email_menubutton.popover.grab_focus(); + } + } + + private void update_displayed_attachments() { bool has_attachments = !this.displayed_attachments.is_empty; this.attachments_button.set_visible(has_attachments); @@ -787,130 +873,22 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return (this.email_store.account.incoming.current_status == CONNECTED); } - /** - * Updates the email menu if it is open. - */ - private void on_email_menu() { - if (this.email_menubutton.active) { - this.email_menu.remove_all(); - - bool supports_trash = get_action_enabled(ACTION_TRASH_MESSAGE); - bool supports_delete = get_action_enabled(ACTION_DELETE_MESSAGE); - bool show_trash_button = !this.shift_key_down && (supports_trash || !supports_delete); - Util.Gtk.menu_foreach(this.email_menu_model, (label, name, target, section) => { - if ((section != this.email_menu_trash || show_trash_button) && - (section != this.email_menu_delete || !show_trash_button)) { - this.email_menu.append_item(new MenuItem.section(label, section)); - } - }); - } - } - - private async void view_source() { - MainWindow? main = get_toplevel() as MainWindow; - if (main != null) { - Geary.Email email = this.email; - try { - yield Geary.Nonblocking.Concurrent.global.schedule_async( - () => { - string source = ( - email.header.buffer.to_string() + - email.body.buffer.to_string() - ); - string temporary_filename; - int temporary_handle = GLib.FileUtils.open_tmp( - "geary-message-XXXXXX.txt", - out temporary_filename - ); - GLib.FileUtils.set_contents(temporary_filename, source); - GLib.FileUtils.close(temporary_handle); - - // ensure this file is only readable by the - // user ... this needs to be done after the - // file is closed - GLib.FileUtils.chmod( - temporary_filename, - (int) (Posix.S_IRUSR | Posix.S_IWUSR) - ); - - string temporary_uri = GLib.Filename.to_uri( - temporary_filename, null - ); - main.application.show_uri.begin(temporary_uri); - }, - null - ); - } catch (GLib.Error error) { - main.application.controller.report_problem( - new Geary.ProblemReport(error) - ); - } - } - } - - private async void print() throws Error { - Json.Builder builder = new Json.Builder(); - builder.begin_object(); - if (this.email.from != null) { - builder.set_member_name(_("From:")); - builder.add_string_value(this.email.from.to_string()); - } - if (this.email.to != null) { - // Translators: Human-readable version of the RFC 822 To header - builder.set_member_name(_("To:")); - builder.add_string_value(this.email.to.to_string()); - } - if (this.email.cc != null) { - // Translators: Human-readable version of the RFC 822 CC header - builder.set_member_name(_("Cc:")); - builder.add_string_value(this.email.cc.to_string()); - } - if (this.email.bcc != null) { - // Translators: Human-readable version of the RFC 822 BCC header - builder.set_member_name(_("Bcc:")); - builder.add_string_value(this.email.bcc.to_string()); - } - if (this.email.date != null) { - // Translators: Human-readable version of the RFC 822 Date header - builder.set_member_name(_("Date:")); - builder.add_string_value( - Util.Date.pretty_print_verbose( - this.email.date.value.to_local(), - this.config.clock_format - ) - ); - } - if (this.email.subject != null) { - // Translators: Human-readable version of the RFC 822 Subject header - builder.set_member_name(_("Subject:")); - builder.add_string_value(this.email.subject.to_string()); - } - builder.end_object(); - Json.Generator generator = new Json.Generator(); - generator.set_root(builder.get_root()); - string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");"; - yield this.primary_message.web_view.run_javascript(js, null); - - Gtk.Window? window = get_toplevel() as Gtk.Window; - WebKit.PrintOperation op = new WebKit.PrintOperation( - this.primary_message.web_view + private void activate_email_action(string name) { + GLib.ActionGroup? email_actions = get_action_group( + ConversationListBox.EMAIL_ACTION_GROUP_NAME ); - Gtk.PrintSettings settings = new Gtk.PrintSettings(); - - if (this.email.subject != null) { - string file_name = Geary.String.reduce_whitespace(this.email.subject.value); - file_name = file_name.replace("/", "_"); - if (file_name.char_count() > 128) { - file_name = Geary.String.safe_byte_substring(file_name, 128); - } - - if (!Geary.String.is_empty(file_name)) { - settings.set(Gtk.PRINT_SETTINGS_OUTPUT_BASENAME, file_name); - } + if (email_actions != null) { + email_actions.activate_action(name, this.email.id.to_variant()); } + } - op.set_print_settings(settings); - op.run_dialog(window); + [GtkCallback] + private void on_email_menu() { + update_email_menu(); + } + + private void on_shift_changed() { + update_email_menu(); } private void on_body_loading_timeout() { @@ -921,12 +899,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.body_loading_timeout.reset(); } - private void on_flag_remote_images(ConversationMessage view) { - if (!email.email_flags.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES)) { - // Don't pass a cancellable in to make sure the flag is - // always saved - mark_email(Geary.EmailFlags.LOAD_REMOTE_IMAGES, null); - } + private void on_flag_remote_images() { + activate_email_action(ConversationListBox.ACTION_MARK_LOAD_REMOTE); } private void on_save_image(string uri, @@ -1000,10 +974,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } } - private void on_view_source() { - this.view_source.begin(); - } - private void on_service_status_change() { if (this.message_body_state == FAILED && !this.load_cancellable.is_cancelled() && diff --git a/src/client/conversation-viewer/conversation-list-box.vala b/src/client/conversation-viewer/conversation-list-box.vala index a2a0f62b..52ca7abf 100644 --- a/src/client/conversation-viewer/conversation-list-box.vala +++ b/src/client/conversation-viewer/conversation-list-box.vala @@ -30,6 +30,24 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { Geary.Email.Field.ORIGINATORS ); + internal const string EMAIL_ACTION_GROUP_NAME = "eml"; + + internal const string ACTION_DELETE = "delete"; + internal const string ACTION_EDIT = "edit"; + internal const string ACTION_FORWARD = "forward"; + internal const string ACTION_MARK_LOAD_REMOTE = "mark-load-remote"; + internal const string ACTION_MARK_READ = "mark-read"; + internal const string ACTION_MARK_STARRED = "mark-starred"; + internal const string ACTION_MARK_UNREAD = "mark-unread"; + internal const string ACTION_MARK_UNREAD_DOWN = "mark-unread-down"; + internal const string ACTION_MARK_UNSTARRED = "mark-unstarred"; + internal const string ACTION_PRINT = "print"; + internal const string ACTION_REPLY_ALL = "reply-all"; + internal const string ACTION_REPLY_SENDER = "reply-sender"; + internal const string ACTION_SAVE_ALL_ATTACHMENTS = "save-all-attachments"; + internal const string ACTION_TRASH = "trash"; + internal const string ACTION_VIEW_SOURCE = "view-source"; + // Offset from the top of the list box which emails views will // scrolled to, so the user can see there are additional messages // above it. XXX This is currently approx 0.5 times the height of @@ -47,6 +65,27 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // mark it as read private const int MARK_READ_PADDING = 50; + private const string ACTION_TARGET_TYPE = ( + Geary.EmailIdentifier.BASE_VARIANT_TYPE + ); + private const ActionEntry[] email_action_entries = { + { ACTION_DELETE, on_email_delete, ACTION_TARGET_TYPE }, + { ACTION_EDIT, on_email_edit, ACTION_TARGET_TYPE }, + { ACTION_FORWARD, on_email_forward, ACTION_TARGET_TYPE }, + { ACTION_MARK_LOAD_REMOTE, on_email_load_remote, ACTION_TARGET_TYPE }, + { ACTION_MARK_READ, on_email_mark_read, ACTION_TARGET_TYPE }, + { ACTION_MARK_STARRED, on_email_mark_starred, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNREAD, on_email_mark_unread, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNREAD_DOWN, on_email_mark_unread_down, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNSTARRED, on_email_mark_unstarred, ACTION_TARGET_TYPE }, + { ACTION_PRINT, on_email_print, ACTION_TARGET_TYPE }, + { ACTION_REPLY_ALL, on_email_reply_all, ACTION_TARGET_TYPE }, + { ACTION_REPLY_SENDER, on_email_reply_sender, ACTION_TARGET_TYPE }, + { ACTION_SAVE_ALL_ATTACHMENTS, on_email_save_all_attachments, ACTION_TARGET_TYPE }, + { ACTION_TRASH, on_email_trash, ACTION_TARGET_TYPE }, + { ACTION_VIEW_SOURCE, on_email_view_source, ACTION_TARGET_TYPE }, + }; + /** Manages find/search term matching in a conversation. */ public class SearchManager : Geary.BaseObject { @@ -492,6 +531,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { private Geary.TimeoutManager mark_read_timer; + private GLib.SimpleActionGroup email_actions = new GLib.SimpleActionGroup(); + /** Keyboard action to scroll the conversation. */ [Signal (action=true)] @@ -536,19 +577,28 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { this.mark_read_timer.start(); } - /** Fired when an email view is added to the conversation list. */ - public signal void email_added(ConversationEmail email); + /** Fired when the user clicks "reply" in the message menu. */ + public signal void reply_to_sender_email(Geary.Email email, string? quote); - /** Fired when an email view is removed from the conversation list. */ - public signal void email_removed(ConversationEmail email); + /** Fired when the user clicks "reply all" in the message menu. */ + public signal void reply_to_all_email(Geary.Email email, string? quote); - /** Fired when the user updates the flags for a set of emails. */ - public signal void mark_emails( - Geary.App.Conversation conversation, - Gee.Collection emails, - Geary.EmailFlags? flags_to_add, - Geary.EmailFlags? flags_to_remove - ); + /** Fired when the user clicks "forward" in the message menu. */ + public signal void forward_email(Geary.Email email, string? quote); + + /** Emitted when email message flags are to be updated. */ + public signal void mark_email(Gee.Collection email, + Geary.NamedFlag? to_add, + Geary.NamedFlag? to_remove); + + /** Fired when the user clicks "trash" in the message menu. */ + public signal void trash_email(Geary.Email email); + + /** Fired when the user clicks "delete" in the message menu. */ + public signal void delete_email(Geary.Email email); + + /** Fired the edit draft button is clicked. */ + public signal void edit_email(Geary.Email email); /** @@ -579,6 +629,9 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { set_adjustment(adjustment); set_sort_func(ConversationListBox.on_sort); + this.email_actions.add_action_entries(email_action_entries, this); + insert_action_group(EMAIL_ACTION_GROUP_NAME, this.email_actions); + this.row_activated.connect(on_row_activated); this.conversation.appended.connect(on_conversation_appended); @@ -923,6 +976,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } ConversationEmail view = new ConversationEmail( + conversation, email, this.email_store, this.contacts, @@ -931,8 +985,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { is_draft(email), this.cancellable ); - view.mark_email.connect(on_mark_email); - view.mark_email_from_here.connect(on_mark_email_from_here); view.internal_link_activated.connect(on_internal_link_activated); view.body_selection_changed.connect((email, has_selection) => { this.body_selected_view = has_selection ? email : null; @@ -957,7 +1009,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } else { insert(row, 0); } - email_added(view); return row; } @@ -967,7 +1018,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { EmailRow? row = null; if (this.email_rows.unset(email.id, out row)) { remove(row); - email_removed(row.view); } } @@ -1048,9 +1098,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { }); if (email_ids.size > 0) { - Geary.EmailFlags flags = new Geary.EmailFlags(); - flags.add(Geary.EmailFlags.UNREAD); - mark_emails(this.conversation, email_ids, null, flags); + mark_email(email_ids, null, Geary.EmailFlags.UNREAD); } } @@ -1098,6 +1146,19 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ); } + private ConversationEmail action_target_to_view(GLib.Variant target) { + Geary.EmailIdentifier? id = null; + try { + id = this.conversation.base_folder.account.to_email_identifier(target); + } catch (Geary.EngineError err) { + debug("Failed to get email id for action target: %s", err.message); + } + debug("XXX have id? %s", (id != null).to_string()); + EmailRow? row = (id != null) ? this.email_rows[id] : null; + debug("XXX have row? %s", (row != null).to_string()); + return (row != null) ? row.view : null; + } + private void on_conversation_appended(Geary.App.Conversation conversation, Geary.Email email) { on_conversation_appended_async.begin(conversation, email); @@ -1135,44 +1196,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { row.view.update_flags(email); } - private void on_mark_email(ConversationEmail view, - Geary.NamedFlag? to_add, - Geary.NamedFlag? to_remove) { - Gee.Collection ids = - new Gee.LinkedList(); - ids.add(view.email.id); - mark_emails( - this.conversation, - ids, - flag_to_flags(to_add), - flag_to_flags(to_remove) - ); - } - - private void on_mark_email_from_here(ConversationEmail view, - Geary.NamedFlag? to_add, - Geary.NamedFlag? to_remove) { - Geary.Email email = view.email; - Gee.Collection ids = - new Gee.LinkedList(); - ids.add(email.id); - this.foreach((row) => { - if (row.get_visible()) { - Geary.Email other = ((EmailRow) row).view.email; - if (Geary.Email.compare_sent_date_ascending( - email, other) < 0) { - ids.add(other.id); - } - } - }); - mark_emails( - this.conversation, - ids, - flag_to_flags(to_add), - flag_to_flags(to_remove) - ); - } - private void on_message_body_state_notify(GLib.Object obj, GLib.ParamSpec param) { ConversationEmail? view = obj as ConversationEmail; @@ -1181,15 +1204,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } } - private Geary.EmailFlags? flag_to_flags(Geary.NamedFlag? flag) { - Geary.EmailFlags flags = null; - if (flag != null) { - flags = new Geary.EmailFlags(); - flags.add(flag); - } - return flags; - } - private void on_row_activated(Gtk.ListBoxRow widget) { EmailRow? row = widget as EmailRow; if (row != null) { @@ -1212,4 +1226,169 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { scroll_to_anchor(row, y); } + // Email action callbacks + + private void on_email_reply_sender(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + reply_to_sender_email(view.email, quote); + }); + } + } + + private void on_email_reply_all(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + reply_to_all_email(view.email, quote); + }); + } + } + + private void on_email_forward(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + forward_email(view.email, quote); + }); + } + } + + private void on_email_mark_read(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + null, + Geary.EmailFlags.UNREAD + ); + } + } + + private void on_email_mark_unread(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.UNREAD, + null + ); + } + } + + private void on_email_mark_unread_down(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + Geary.Email email = view.email; + var ids = new Gee.LinkedList(); + ids.add(email.id); + this.foreach((row) => { + if (row.get_visible()) { + Geary.Email other = ((EmailRow) row).view.email; + if (Geary.Email.compare_sent_date_ascending( + email, other) < 0) { + ids.add(other.id); + } + } + }); + mark_email(ids, Geary.EmailFlags.UNREAD, null); + } + } + + private void on_email_mark_starred(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.FLAGGED, + null + ); + } + } + + private void on_email_mark_unstarred(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + null, + Geary.EmailFlags.FLAGGED + ); + } + } + + private void on_email_load_remote(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.LOAD_REMOTE_IMAGES, + null + ); + } + } + + private void on_email_edit(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + edit_email(view.email); + } + } + + private void on_email_trash(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + trash_email(view.email); + } + } + + private void on_email_delete(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + delete_email(view.email); + } + } + + private void on_email_save_all_attachments(GLib.SimpleAction action, + GLib.Variant? param) { + debug("XXX save all: %s", param.print(true)); + ConversationEmail? view = action_target_to_view(param); + if (view != null && view.attachments_pane != null) { + debug("XXX really save all"); + view.attachments_pane.save_all(); + } + } + + private void on_email_print(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.print.begin(); + } + } + + private void on_email_view_source(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.view_source.begin(); + } + } + } diff --git a/src/client/util/util-gtk.vala b/src/client/util/util-gtk.vala index e25c319c..798ad0ed 100644 --- a/src/client/util/util-gtk.vala +++ b/src/client/util/util-gtk.vala @@ -84,6 +84,83 @@ namespace Util.Gtk { return widget.get_allocated_height() - margin.top - margin.bottom; } + /** + * Constructs a frozen GMenu from an existing model using a visitor. + * + * The visitor is applied to the given template model to each of + * its items, or recursively for any section or submenu. If the + * visitor returns false when passed an item, section or submenu + * then it will be skipped, otherwise it will be added to a new + * menu. + * + * The constructed menu will be returned frozen. + * + * @see MenuVisitor + */ + public GLib.Menu construct_menu(GLib.MenuModel template, + MenuVisitor visitor) { + GLib.Menu model = new GLib.Menu(); + for (int i = 0; i < template.get_n_items(); i++) { + GLib.MenuItem item = new GLib.MenuItem.from_model(template, i); + string? action = null; + GLib.Variant? action_value = item.get_attribute_value( + GLib.Menu.ATTRIBUTE_ACTION, GLib.VariantType.STRING + ); + if (action_value != null) { + action = (string) action_value; + } + GLib.Menu? section = (GLib.Menu) item.get_link( + GLib.Menu.LINK_SECTION + ); + GLib.Menu? submenu = (GLib.Menu) item.get_link( + GLib.Menu.LINK_SUBMENU + ); + + bool append = false; + if (section != null) { + if (visitor(template, section, action, item)) { + append = true; + section = construct_menu(section, visitor); + item.set_section(section); + } + } else if (submenu != null) { + if (visitor(template, submenu, action, item)) { + append = true; + submenu = construct_menu(submenu, visitor); + item.set_submenu(submenu); + } + } else { + append = visitor(template, null, action, item); + } + + if (append) { + model.append_item(item); + } + } + model.freeze(); + return model; + } + + /** + * Visitor for {@link construct_menu}. + * + * Implementations should return true to accept the given child + * menu or menu item, causing it to be included in the new model, + * or false to reject it and cause it to be skipped. + * + * @param existing_menu - current menu or submenu being visited + * @param existing_child_menu - if not null, a child menu that is + * about to be descended into + * @param existing_action - existing fully qualified action name + * of the curent item, if any + * @param new_item - copy of the menu item being visited, which if + * accepted will be added to the new model + */ + public delegate bool MenuVisitor(GLib.MenuModel existing_menu, + GLib.MenuModel? existing_child_menu, + string? existing_action, + GLib.MenuItem? new_item); + /** Copies a GLib menu, setting targets for the given actions. */ public GLib.Menu copy_menu_with_targets(GLib.Menu template, string group, diff --git a/ui/conversation-email-menus.ui b/ui/conversation-email-menus.ui index ecf3176d..5681cec0 100644 --- a/ui/conversation-email-menus.ui +++ b/ui/conversation-email-menus.ui @@ -7,13 +7,13 @@ _Reply - eml.reply_sender + eml.reply-sender mail-reply-sender-symbolic Reply to _All - eml.reply_all + eml.reply-all mail-reply-all-symbolic @@ -28,19 +28,19 @@ _Mark Read - eml.mark_read + eml.mark-read _Mark Unread - eml.mark_unread + eml.mark-unread Mark Unread From _Here - eml.mark_unread_down + eml.mark-unread-down
@@ -48,14 +48,14 @@ Move message to _Trash - eml.trash_msg + eml.trash
_Delete message… - eml.delete_msg + eml.delete
@@ -67,7 +67,7 @@ _View Source - eml.view_source + eml.view-source
diff --git a/ui/conversation-email.ui b/ui/conversation-email.ui index 156f1b0f..f3955dd0 100644 --- a/ui/conversation-email.ui +++ b/ui/conversation-email.ui @@ -26,7 +26,7 @@ True Save all attachments start - eml.save_all_attachments + eml.save-all-attachments none @@ -49,7 +49,7 @@ True Mark this message as starred start - eml.star + eml.mark-starred none @@ -71,7 +71,7 @@ True Mark this message as not starred start - eml.unstar + eml.mark-unstarred none @@ -92,9 +92,9 @@ False True True - Display the message menu start none + True